The answer by Ilja Everilä is already a best possible. While it does not store the value of class_id
inside the table literally, please observe that any two instances of the same class always have the same value of class_id
. So knowing the class is sufficient to compute the class_id
for any given item. In the code example that Ilja provided, the type
column ensures that the class can always be known and the class_id
class property takes care of the rest. So the class_id
is still represented in the table, if indirectly.
I repeat Ilja's example from his original answer here, in case he decides to change it in his own post. Let us call this "solution 1".
class Item(Base):
name = 'unnamed item'
@classproperty
def class_id(cls):
return '.'.join((cls.__module__, cls.__qualname__))
__tablename__ = 'item'
id = Column(Integer, primary_key=True)
type = Column(String(50))
__mapper_args__ = {
'polymorphic_identity': 'item',
'polymorphic_on': type
}
class Sword(Item):
name = 'Sword'
__tablename__ = 'sword'
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
durability = Column(Integer, default=100)
__mapper_args__ = {
'polymorphic_identity': 'sword',
}
class Pistol(Item):
name = 'Pistol'
__tablename__ = 'pistol'
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
ammo = Column(Integer, default=10)
__mapper_args__ = {
'polymorphic_identity': 'pistol',
}
Ilja hinted at a solution in his last comment to the question, using @declared_attr
, which would literally store the class_id
inside the table, but I think it would be less elegant. All it buys you is representing the exact same information in a slightly different way, at the cost of making your code more complicated. See for yourself ("solution 2"):
class Item(Base):
name = 'unnamed item'
@classproperty
def class_id_(cls): # note the trailing underscore!
return '.'.join((cls.__module__, cls.__qualname__))
__tablename__ = 'item'
id = Column(Integer, primary_key=True)
class_id = Column(String(50)) # note: NO trailing underscore!
@declared_attr # the trick
def __mapper_args__(cls):
return {
'polymorphic_identity': cls.class_id_,
'polymorphic_on': class_id
}
class Sword(Item):
name = 'Sword'
__tablename__ = 'sword'
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
durability = Column(Integer, default=100)
@declared_attr
def __mapper_args__(cls):
return {
'polymorphic_identity': cls.class_id_,
}
class Pistol(Item):
name = 'Pistol'
__tablename__ = 'pistol'
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
ammo = Column(Integer, default=10)
@declared_attr
def __mapper_args__(cls):
return {
'polymorphic_identity': cls.class_id_,
}
There is also an additional danger in this approach, which I will discuss later.
In my opinion, it would be more elegant to make the code simpler. This could be achieved by starting with solution 1 and then merging the name
and type
properties, since they are redundant ("solution 3"):
class Item(Base):
@classproperty
def class_id(cls):
return '.'.join((cls.__module__, cls.__qualname__))
__tablename__ = 'item'
id = Column(Integer, primary_key=True)
name = Column(String(50)) # formerly known as type
__mapper_args__ = {
'polymorphic_identity': 'unnamed item',
'polymorphic_on': name,
}
class Sword(Item):
__tablename__ = 'sword'
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
durability = Column(Integer, default=100)
__mapper_args__ = {
'polymorphic_identity': 'Sword',
}
class Pistol(Item):
__tablename__ = 'pistol'
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
ammo = Column(Integer, default=10)
__mapper_args__ = {
'polymorphic_identity': 'Pistol',
}
All three solutions discussed so far give you the exact same requested behaviour on the Python side (assuming that you will ignore the type
attribute). For example, an instance of Pistol
will return 'yourmodule.Pistol'
as its class_id
and 'Pistol'
as its name
in each solution. Also in each solution, if you add a new item class to the hierarchy, say Key
, all its instances will automatically report their class_id
to be 'yourmodule.Key'
and you will be able to set their common name
once at the class level.
There are some subtle differences on the SQL side, regarding the name and value of the column that disambiguates between the item classes. In solution 1, the column is called type
and its value is arbitrarily chosen for each class. In solution 2, the column name is class_id
and its value is equal to the class property, which depends on the class name. In solution 3, the name is name
and its value is equal to the name
property of the class, which can be varied independently from the class name. However, since all these different ways to disambiguate the item class can be mapped one-to-one to each other, they contain the same information.
I mentioned before that there is a catch in the way solution 2 disambiguates the item class. Suppose that you decide to rename the Pistol
class to Gun
. Gun.class_id_
(with trailing underscore) and Gun.__mapper_args__['polymorphic_identity']
will automatically change to 'yourmodule.Gun'
. However, the class_id
column in your database (mapped to Gun.class_id
without trailing underscore) will still contain 'yourmodule.Pistol'
. Your database migration tool might not be smart enough to figure out that those values need to be updated. If you are not careful, your class_id
s will be corrupted and SQLAlchemy will likely throw exceptions at you for being unable to find matching classes for your items.
You could avoid this problem by using an arbitrary value as the disambiguator, as in solution 1, and storing the class_id
in a separate column using @declared_attr
magic (or a similar indirect route), as in solution 2. However, at this point you really need to ask yourself why the class_id
needs to be in the database table. Does it really justify making your code so complicated?
Take home message: you can map plain class attributes as well computed class properties using SQLAlchemy, even in the face of inheritance, as illustrated by the solutions. That does not necessarily mean you should actually do it. Start with your end goals in mind, and find the simplest way to achieve those goals. Only make your solution more sophisticated if doing so solves a real problem.