SQLAlchemy: Subclassed model db entry gets properties of other subclassed model - python

I'm having an issue with SQLAlchemy in a Flask app where I have two models Instructor and Student that subclass the same model User. When I'm creating a Student object, it's listed in the database under User with properties that should be unique for Instructor. Here are my (simplified) classes:
class User(db.Model):
__tablename__ = "user"
discriminator = db.Column("type", db.String(50)) # Student or Instructor
__mapper_args__ = {"polymorphic_on": discriminator}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
class Instructor(User):
__mapper_args__ = {"polymorphic_identity": "instructor"}
reputation = db.Column(db.Integer, default=1)
approved_for_teaching = db.Column(db.Boolean, default=False)
class Student(User):
__mapper_args__ = {"polymorphic_identity": "student"}
lessons_taken = db.Column(db.Integer, default=0)
I'm creating a Student like this:
new_student = Student(name=user_name)
db.session.add(new_student)
db.session.commit()
But when inspecting the object in the database, the student gets attributes from the Instructor model, i.e. reputation (value is 1) and approved_for_teaching (value is False). Am I doing something wrong here, or is this expected behaviour? I could understand if the columns would have to be present, since they share the same table (User) in the db, but then I'd expect the values to be null or something. Thanks!

It is expected behaviour, since you're using single table inheritance:
Single table inheritance represents all attributes of all subclasses within a single table. A particular subclass that has attributes unique to that class will persist them within columns in the table that are otherwise NULL if the row refers to a different kind of object.
Though the documentation mentions leaving the columns belonging to other classes NULL, the client side defaults you've given the columns kick in during inserts to user table, which underlies all the classes inheriting from User, even when they're not a part of the particular subclass' columns. A context sensitive default function could perhaps be used to avoid that, for example:
def polymorphic_default(discriminator, identity, value):
def default(context):
# This should be replaced with context.get_current_parameters() in 1.2 and above
if context.current_parameters[discriminator.name] == identity:
return value
return default
...
class Instructor(User):
__mapper_args__ = {"polymorphic_identity": "instructor"}
reputation = db.Column(
db.Integer, default=polymorphic_default(User.discriminator, 'instructor', 1))
approved_for_teaching = db.Column(
db.Boolean, default=polymorphic_default(User.discriminator, 'instructor', False))
but that seems like a lot of work to avoid a rather small issue.

Related

Set an attribute inside the model of a one-to-many relationship in SQLAlchemy

I'm trying to set a relationship attribute inside the parent model (Articles) and return the most recent object of the RelevantFlag. The Articles and RelevantFlag models are a one-to-many relationship thus my models look like this:
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
relevant_flag = db.relationship(
'RelevantFlag',
backref="Article",
lazy='dynamic'
)
# Get the latest RelevantFlag model object
def getLatestRelevance(self):
return Article.query.join(
RelevantFlag
).filter(
RelevantFlag.id == self.id
).order_by(
Article.id.desc()
).first()
def setRelevance(self, state):
# Set the boolean to what's passed in 'state'
def __repr__(self):
return '<Article {}>'.format(self.id)
class RelevantFlag(db.Model):
id = db.Column(db.Integer, primary_key=True)
article_id = db.Column(db.Integer, db.ForeignKey('article.id'))
state = db.Column(db.Boolean)
My question is, how do I write a setter to change the RelevantFlag's state attribute inside the Articles model?
If I understood correctly you want to change a column value by accessing a one-to-many relationship?
def setRelevance(self, state):
# Set the boolean to what's passed in 'state'
self.relevant_flag.state = state

SQLalchemy setting constraints on relationships in many-to-many

Suppose I have a set of users and each user has access to a collection of tools. The same tool might have many users with access so this is a many-to-many relationship:
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
tools = db.relationship("Tool", secondary=user_tool_assoc_table,
back_populates='users')
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=False)
user_tool_assoc_table = db.Table('user_tool', db.Model.metadata,
db.Column('user', db.Integer, db.ForeignKey('user.id')),
db.Column('tool', db.Integer, db.ForeignKey('tool.id')))
Observe that user names are unique, but tool names are not. So User.name:Mike1 and User.name:Mike2 might have access to Tool.name:Hammer, and, separately, User.name:John1 and User.name:John2 might have access to Tool.name:Hammer by the same name but each with different Tool.ids.
I want to make a constraint that within the User.tools collection there can never be a tool with the same name as another, i.e.
A user cannot create a new Tool as part of his collection if one with that name already exists. Mike1 cannot create a new tool called Hammer that forms part of his tools collection.
A Tool that exists in the database cannot be appended to the tools collection of a user if one with the same name already exists in the set, i.e. John1's Hammer cannot be shared with Mike1 since Mike1 already has his own Hammer.
James, however, can create a new Hammer since he does not already have a hammer. There will then be 3 tools in the database called Hammer each with a distinct set of Users.
Note in my specific case a Tool will only exist if it has at least one User, but I also don't know how to ensure this natively in my database.
Is this possible natively with SQLalchemy to automatically configure my database to maintain integrity? I don't want to write my own validator rules since I will likely miss something and end up with a database which breaks my rules.
The problem is how to express the predicate "A user identified by ID has only one tool with the name NAME". This would of course be easy to express with a simple table such as:
db.Table('user_toolname',
db.Column('user', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('toolname', db.String, primary_key=True))
It is also very clear that this alone is not nearly enough to uphold integrity, as there is no connection between the fact about user's toolnames and the actual tools. Your database could state that a user both has a hammer and doesn't have a hammer.
It would be nice to enforce this in your user_tool_assoc_table or something equivalent, but since Tool.name is not a part of the primary key of Tool, you cannot reference it. On the other hand since you do want to allow multiple tools with the same name to co-exist, the subset { id, name } is in fact the proper key for Tool:
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, primary_key=True)
The id now acts as a "discriminator" of sorts between the tools having the same name. Note that id need not be globally unique in this model, but locally to name. It's handy to have it auto increment still, but the default setting of autoincrement='auto' only treats a single-column integer primary key as having auto incrementing behavior by default, so it must be explicitly set.
It is now possible to define user_tool_assoc_table in terms of tool_name as well, with the additional constraint that a user can only have a single tool with a given name:
user_tool_assoc_table = db.Table(
'user_tool',
db.Column('user', db.Integer, db.ForeignKey('user.id')),
db.Column('tool', db.Integer),
db.Column('name', db.String),
db.ForeignKeyConstraint(['tool', 'name'],
['tool.id', 'tool.name']),
db.UniqueConstraint('user', 'name'))
With this model and the following setup:
john = User(name='John')
mark = User(name='Mark')
db.session.add_all([john, mark])
hammer1 = Tool(name='Hammer')
hammer2 = Tool(name='Hammer')
db.session.add_all([hammer1, hammer2])
db.session.commit()
This will succeed:
john.tools.append(hammer1)
hammer2.users.append(mark)
db.session.commit()
And this will fail after the above, since it violates the unique constraint:
john.tools.append(hammer2)
db.session.commit()
If you want to model the domain by allowing tool names to be non-unique, then there is no easy way to accomplish this.
You can try adding a validator to the User model which will check the User.tools list during every append and make sure that it obeys a certain condition
from sqlalchemy.orm import validates
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
tools = db.relationship("Tool", secondary=user_tool_assoc_table,
back_populates='users')
#validates('tools')
def validate_tool(self, key, tool):
assert tool.name not in [t.name for t in self.tools]
return tool
def __repr__(self):
return self.name
The above approach will make sure that if you add a new tool which has the same name as an existing tools in user.tools list it will throw an exception. But the problem is that you can still directly assign a new list with duplicate tools directly like this
mike.tools = [hammer1, hammer2, knife1]
This will work because validates works only during append operation. Not during assignment. If we want a solution that works even during assignment, then we will have to figure out a solution where user_id and tool_name will be in the same table.
We can do this by making the secondary association table have 3 columns user_id, tool_id and tool_name. We can then make tool_id and tool_name to behave as a Composite Foreign Key together (Refer https://docs.sqlalchemy.org/en/latest/core/constraints.html#defining-foreign-keys)
By this approach, the association table will have a standard foreign key to user_id and then a composite foreign key constraint which combines tool_id and tool_name. Now that both keys are there in the association table, we can then proceed to define an UniqueConstraint on the table which will make sure that user_id and tool_name will have to be an unique combination
Here is the code
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.orm import validates
from sqlalchemy.schema import ForeignKeyConstraint, UniqueConstraint
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLAlchemy(app)
user_tool_assoc_table = db.Table('user_tool', db.Model.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('tool_id', db.Integer),
db.Column('tool_name', db.Integer),
ForeignKeyConstraint(['tool_id', 'tool_name'], ['tool.id', 'tool.name']),
UniqueConstraint('user_id', 'tool_name', name='unique_user_toolname')
)
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
tools = db.relationship("Tool", secondary=user_tool_assoc_table,
back_populates='users')
def __repr__(self):
return self.name
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=False)
users = db.relationship("User", secondary=user_tool_assoc_table,
back_populates='tools')
def __repr__(self):
return "{0} - ID: {1}".format(self.name, self.id)
db.create_all()
mike=User(name="Mike")
pete=User(name="Pete")
bob=User(name="Bob")
db.session.add_all([mike, pete, bob])
db.session.commit()
hammer1 = Tool(name="hammer")
hammer2 = Tool(name="hammer")
knife1 = Tool(name="knife")
knife2 = Tool(name="knife")
db.session.add_all([hammer1, hammer2, knife1, knife2])
db.session.commit()
Now let's try playing around
In [2]: users = db.session.query(User).all()
In [3]: tools = db.session.query(Tool).all()
In [4]: users
Out[4]: [Mike, Pete, Bob]
In [5]: tools
Out[5]: [hammer - ID: 1, hammer - ID: 2, knife - ID: 3, knife - ID: 4]
In [6]: users[0].tools = [tools[0], tools[2]]
In [7]: db.session.commit()
In [9]: users[0].tools.append(tools[1])
In [10]: db.session.commit()
---------------------------------------------------------------------------
IntegrityError Traceback (most recent call last)
<ipython-input-10-a8e4ec8c4c52> in <module>()
----> 1 db.session.commit()
/home/surya/Envs/inkmonk/local/lib/python2.7/site-packages/sqlalchemy/orm/scoping.pyc in do(self, *args, **kwargs)
151 def instrument(name):
152 def do(self, *args, **kwargs):
--> 153 return getattr(self.registry(), name)(*args, **kwargs)
154 return do
So appending a tool of the same name throws exception.
Now let's try assigning a list with duplicate tool names
In [14]: tools
Out[14]: [hammer - ID: 1, hammer - ID: 2, knife - ID: 3, knife - ID: 4]
In [15]: users[0].tools = [tools[0], tools[1]]
In [16]: db.session.commit()
---------------------------------------------------------------------------
IntegrityError Traceback (most recent call last)
<ipython-input-16-a8e4ec8c4c52> in <module>()
----> 1 db.session.commit()
/home/surya/Envs/inkmonk/local/lib/python2.7/site-packages/sqlalchemy/orm/scoping.pyc in do(self, *args, **kwargs)
151 def instrument(name):
152 def do(self, *args, **kwargs):
--> 153 return getattr(self.registry(), name)(*args, **kwargs)
154 return do
This throws an exception as well. So we have made sure at db level that your requirement is solved.
But in my opinion, taking such a convoluted approach usually indicates that we are needlessly complicating the design. If you are ok with changing the table design, please consider the following suggestion for a simpler approach.
In my opinion, it is better to have a set of unique tools and a set of unique users and then model a M2M relationship between them. Any property which is specific to Mike's hammer, but not present in James' hammer should be a property of that association between them.
If you take that approach, you have a set of users like this
Mike, James, John, George
and a set of tools like this
Hammer, Screwdriver, Wedge, Scissors, Knife
And you can still model a many to many relation between them. In this scenario, the only change you have to do is to set unique=True on the Tool.name column, so that there is only one hammer globally which can have that name.
If you need Mike's hammer to have some unique properties distinct from James's Hammer, then you can just add some extra columns in the association table. To access user.tools and tool.users, you can use an association_proxy.
from sqlalchemy.ext.associationproxy import association_proxy
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
associated_tools = db.relationship("UserToolAssociation")
tools = association_proxy("associated_tools", "tool")
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
associated_users = db.relationship("UserToolAssociation")
users = association_proxy("associated_users", "user")
class UserToolAssociation(db.Model):
__tablename__ = 'user_tool_association'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
tool_id = db.Column(db.Integer, db.ForeignKey('tool.id'))
property1_specific_to_this_user_tool = db.Column(db.String(20))
property2_specific_to_this_user_tool = db.Column(db.String(20))
user = db.relationship("User")
tool = db.relationship("Tool")
The above approach is better because of the proper separation of concerns. In future when you need to do something that will affect all hammers, you can just modify the hammer instance in the Tools table. If you keep all hammers as separate instances without any link between them, it will become cumbersome to do any modification on them as a whole in the future.

How can I determine the type (e.g. many-to-one) of a dynamic SQLAlchemy relationship?

Suppose I have the following SQLAlchemy classes defined:
Base = declarative_base()
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
computers = relationship('Computer', backref=backref('owner', lazy='dynamic'))
class Computer(Base):
__tablename__ = 'computer'
id = Column(Integer, primary_key=True)
ownerid = Column(Integer, ForeignKey('person.id'))
Suppose further that I have accessed the lazy query object this way:
relation = getattr(Computer, 'owner')
How can I determine if relation refers to a single instance of Person (that is, in a many-to-one relationship, like in this example), or if relation refers to a collection of instances (like in a one-to-many relationship)? In other words, how can I determine the relationship type of a dynamic SQLAlchemy relationship object?
If we suppose model = Computer and relation = 'owner' as in the question, then the following attribute is True if and only if the relation is a list of instances as opposed to a single instance:
model._sa_class_manager[relation].property.uselist
You can then use this to test whether or not to call the one() method on the result of getattr(model, relation):
if model._sa_class_manager[relation].property.uselist:
related_instances = getattr(model, relation)
else:
related_instance = getattr(model, relation).one()
I am not confident, however, that this is the best solution.

How to set the foreign key to a default value on delete?

How to auto set the product category_id to a default value when category is deleted? For example 1 to point to the first category.
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
content = db.Column(db.Text(), unique=True)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
atime = db.Column(db.DateTime())
def __init__(self, name, content, category_id):
self.name = name
self.content = content
self.category_id = category_id
self.atime = datetime.now()
def __repr__(self):
return '<Product %r>' % self.id
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
products = db.relationship('Product', backref='category', cascade="all, delete, delete-orphan")
def __init__(self, *args, **kwargs):
if len(kwargs) > 0:
self.name = kwargs['name']
def __repr__(self):
return '<Category %r>' % self.name
I don't want to use cascade to delete them!
There are two things that need to be performed in orchestration here:
Define the Foreign Key with proper referential action
Configure the cascade option of the SA relationship
I think the cleanest way would be to set the category_id to a NULL value when its Category is deleted: SET NULL is one of the possible Referential Actions of the ON DELETE clause, which you can add to your ForeignKey definition:
category_id = db.Column(db.Integer, db.ForeignKey('category.id', ondelete='SET NULL'))
In the same way you can use an option SET DEFAULT, but in this case you need to also configure the default value for the column category_id: category_id = Column(..., server_default=1). Note that implementations of these differ between different RDBMS.
As to the cascade option: You should basically remove the cascade="all, delete, delete-orphan" from your products relationship definition. In fact, you want to ensure that delete, delete-orphan are not there.
Having said that, you really need to test your code to cover different scenarios, as two different deletions of the Category object might produce different results depending on your RDBMS and SA configuration:
# scenario-1: delete in session: SA might set category_id of all chilren Products to None
c1 = session.query(Category).get(1)
session.delete(c1)
session.commit()
# scenario-2: delete without loading an object into the session: SA will perform no additional logic
session.query(Category).filter(Category.id == 2).delete()
session.commit()
Hope all this points you in the right direction. As usual, enabled SQL logging in your test code using echo=True or just by configuring logging module, and you will see what SA is doing to your database. Other changes which which you did not see in the SQL were done by RDBMS itself given your Referential Action.

Inheritance issue in SQLAlchemy

I need some help with understanding how inheritance works in SQLAlchemy. I've created a base class for a user with some basic functionality. And then some specific users (admin, cooluser, uncooluser). Each one has unique functionality so I decided to use inheritance in SQLAlchemy. The problem is that I need to be able to upgrade a user to cooluser or uncooluser, and downgrade a cooluser to user, at any time,.
class User(Base):
__tablename__ = 'tbl_users'
__table_args__ = {'mysql_engine': 'InnoDB'}
user_id = Column(Integer, primary_key = True, unique = True, nullable = False)
user_name = Column(String(100), nullable = False, unique = True)
password = Column(String(100), nullable = False)
user_type = Column('user_type', String(50))
first_name = Column(String(50), nullable = False)
last_name = Column(String(50), nullable = False)
address = Column(String(50), nullable = False)
city = Column(String(20), nullable = False)
postal_code = Column(String(10), nullable = False)
country_id = Column(Integer, ForeignKey(Contries.country_id))
country = relationship('Country', backref = 'users')
query = Session.query_property()
__mapper_args__ = {'polymorphic_on': user_type, 'polymorphic_identity': 'User'}
class CoolUser(User):
__tablename__ = 'tbl_cool_user'
__table_args__ = {'mysql_engine': 'InnoDB'}
__mapper_args__ = {'polymorphic_identity': 'CoolUser'}
cool_user_id = Column(Integer, ForeignKey(User.user_id, ondelete = 'CASCADE'), primary_key = True)
cool_user_balance = Column(Numeric(15, 3))
Can I create a CoolUser without creating a new row in 'tbl_users', but use an existing one? Can change some setting so that when a CoolUser gets removed it just removes the entry in 'tbl_cool_user', not 'tbl_user'?
Am I missing the point of inheritance in SQLAlchemy?
Am I missing the point of inheritance in SQLAlchemy?
I think that you misusing the inheritance concept in general, and not even in SA implementation specifics.
In a classical Animal (Cat, Dog (Terier)) type of hierarchy would you ever imagine that a Cat would be up/down-graded to a Dog, or even a specific type of a Dog? I don't think so. Also, I can well imagine that some CoolUser could also be an Administrator at the same time. How will you resolve this type of relationship?
Therefore, for your requirements, inheritance is just not the concept to employ to solve this state-changing model. I would suggest to google for User-Role type of relationships and implementations. You would still have to resolve the issue of storing Role-specific data thought.
As #aquavitae mentioned: in SA, you can hack-around and change the user_type. But be aware that in this case the following will happen:
when you load the object from the database, its class will reflect the new type (GOOD)
when you downgrade the object (from CoolUser to User), the row which corresponds to the CoolUser will not be deleted (i think it is BAD, but it might be OK)
when you upgrade the object (from User to CoolUser), no new row for the CoolUser table will be created, and all your values will be NULL. As such, setting/adding any property that is stored in the non-created row will throw a nice error. And when queries for the specific subclass, you will not recieve the object back as the INNER JOIN is used to retrieve the object. (BAD)
in summary, do not change a Cat to a Dog
I don't think you should be using 'polymorphic_on' and 'polymorphic_identity' in the same table. What you need to do is create a base table User containing all users (i.e. with mapper_args 'polymorphic_on'), then CoolUser subclassed from that (with 'polymorphic_identity'):
class User(Base):
...
__mapper_args__ = {'polymorphic_on': user_type}
class CoolUser(User):
...
__mapper_args__ = {'polymorphic_identity': 'CoolUser'}
Then you can change user_type as you like.
You have to use Concrete Table Inheritance. All all common attribute in base table and create 2 different CoolUser and UnCoolUser.

Categories

Resources