I tried the following, and it seems to work:
class BaseModel(db.Model):
__abstract__ = True
row_ver = db.Column(db.Integer, nullable=False)
#declared_attr
def __mapper_args__(cls):
return {'version_id_col': cls.row_ver}
def to_dict(self):
res = dict()
for c in self.__table__.columns:
value = getattr(self, c.name)
if isinstance(value, date):
res[c.name] = value.isoformat()
elif isinstance(value, uuid.UUID):
res[c.name] = str(value)
else:
res[c.name] = value
return res
class Account(BaseModel):
__tablename__ = 'account'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
I'm not sure whether it is a proper way of augmenting the declarative base class, that is db.Model class. Is there anything wrong with the above code?
Also related: Is it possible to create custom declarative base by inheriting from db.Model (which itself is declarative base), something like below:
class Base(db.Model):
#some code here
from sqlalchemy.ext.declarative import declarative_base
BaseModel = declarative_base(cls=Base)
class Account(BaseModel)
#...
To customize db.Model, declare a superclass of flask_sqlalchemy.Model and pass it to model_class as shown below.
from flask_sqlalchemy import Model, SQLAlchemy
class AppModel(Model):
row_ver = db.Column(db.Integer, nullable=False)
db = SQLAlchemy(model_class=AppModel)
class Account(db.Model):
pass
class Thing(db.Model):
pass
Note that Account and Thing inherit from db.Model, not AppModel.
Related
If I try to pass a class "Human" to a method while defining the static attributes of that class, I get the error "Undefined variable: Human".
=>How can I access the current class (-name) to pass it to the shown method?
(In line 7 there is no instance of the class Human available. Therefore this is a different question than Getting the class name of an instance?)
I could pass the class name as hard coded string "Human" but I would like to avoid hard coded strings if possible.
Screenshot:
Human class:
from h_database_orm.domain.entity import Entity
class Human(Entity):
first_name = Entity.string()
last_name = Entity.string()
spouse_id = Entity.foreign_key(Human)
spouse = Entity.one_to_one_attribute(Human)
Parent class Entity:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy import Column, Integer, Float, String, ForeignKey
from sqlalchemy.orm import relation
class AbstractEntity():
#declared_attr
def __tablename__(self):
return AbstractEntity.table_name_for_class(self)
id = Column(Integer, primary_key=True)
#staticmethod
def table_name_for_class(clazz):
return clazz.__name__.lower()
#staticmethod
def integer():
return Column(Integer)
#staticmethod
def float():
return Column(Float)
#staticmethod
def string():
return Column(String)
#staticmethod
def foreign_key(referencedClass):
table_name = AbstractEntity.table_name_for_class(referencedClass)
foreign_key_path = table_name + '.id'
return Column(Integer, ForeignKey(foreign_key_path))
#staticmethod
def one_to_one_attribute(referenced_class):
class_name = referenced_class.__name__
return relation(class_name, uselist=False)
Entity = declarative_base(cls=AbstractEntity)
Ugly work around ... but seems to work:
from h_database_orm.domain.entity import Entity
class Human(Entity):
first_name = Entity.string()
Human.last_name = Entity.string()
Human.spouse_id = Entity.foreign_key(Human)
Human.spouse = Entity.one_to_one_attribute(Human)
I'm using joind table inheritance here.
class BaseEntity(Base):
some_col = Column(String)
base_relationship = relationship("some_relationship", backref="depends_on_who_inherits_me")
class SubEntity(BaseEntity):
some_unique_col = Column(String)
Because the specific backref name will only become known in the run-time(in this case, it should be SubEntity, but it should be able to be inheritable by unlimited subclasses), I need depends_on_who_inherits_me part a variable, or more specifically, the inheriting sub-class's name, instead of a string. So each sub-class will have a relationship referring to a third-party class, while having it referring back to that particular sub-class by its appropriate name.
However, because this is outside of any method, I can't use self to flexibly refer to the instances.
How to implement this idea? Thanks.
One way you might achieve this is with a Mixin that uses declared_attr.cascading.
Here's the mixin class:
class Mixin:
#declared_attr.cascading
def related_entity(cls):
if has_inherited_table(cls):
return relationship(
'RelatedEntity',
backref=cls.__name__.lower(),
uselist=False
)
The cascading flag on declared_attr will make sqlalchemy attempt to render the 'mixed in' attribute on every class in the hierarchy. Or as the docs put it:
This is a special-use modifier which indicates that a column or
MapperProperty-based declared attribute should be configured
distinctly per mapped subclass, within a mapped-inheritance scenario.
The has_inherited_table() function, allows us to determine within the mixin if we are dealing with the BaseEntity or a subclass, so that we only add the relationships on to the subclasses.
The mixin is then inherited into the BaseEntity model:
class BaseEntity(Base, Mixin):
id = sa.Column(sa.Integer, primary_key=True)
related_id = sa.Column(
sa.Integer, sa.ForeignKey('relatedentity.id'))
discriminator = sa.Column(sa.String)
#declared_attr
def __mapper_args__(cls):
if has_inherited_table(cls):
args = {'polymorphic_identity': cls.__name__.lower()}
else:
args = {'polymorphic_on': cls.discriminator}
return args
As you mentioned in your question that you are using joined table inheritance, I've defined the __mapper_args__ on BaseEntity using a #declared_attr method so that the polymorphic_identity also can be automatically generated from the class name for the subclasses.
So with this configuration, every subclass of BaseEntity will apply a relationship attribute on RelatedEntity named after the subclass. Here's the full working example:
import sqlalchemy as sa
from sqlalchemy.ext.declarative import (declarative_base, declared_attr,
has_inherited_table)
from sqlalchemy.orm import relationship, sessionmaker
class BaseClass:
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
Base = declarative_base(cls=BaseClass)
engine = sa.create_engine('sqlite://', echo=False)
Session = sessionmaker(bind=engine)
class Mixin:
#declared_attr.cascading
def related_entity(cls):
if has_inherited_table(cls):
return relationship(
'RelatedEntity',
backref=cls.__name__.lower(),
uselist=False
)
class BaseEntity(Base, Mixin):
id = sa.Column(sa.Integer, primary_key=True)
related_id = sa.Column(
sa.Integer, sa.ForeignKey('relatedentity.id'))
discriminator = sa.Column(sa.String)
#declared_attr
def __mapper_args__(cls):
if has_inherited_table(cls):
args = {'polymorphic_identity': cls.__name__.lower()}
else:
args = {'polymorphic_on': cls.discriminator}
return args
class RelatedEntity(Base):
""" Class that is related to all `BaseEntity` subclasses"""
id = sa.Column(sa.Integer, primary_key=True)
class SubEntity(BaseEntity):
""" Will generate `RelatedEntity.subentity`"""
id = sa.Column(sa.Integer, sa.ForeignKey('baseentity.id'),
primary_key=True)
class OtherEntity(BaseEntity):
""" Will generate `RelatedEntity.otherentity`"""
id = sa.Column(sa.Integer, sa.ForeignKey('baseentity.id'),
primary_key=True)
if __name__ == '__main__':
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
s = Session()
rel_inst = RelatedEntity()
s.add(rel_inst)
rel_inst.subentity.append(SubEntity())
rel_inst.otherentity.append(OtherEntity())
s.commit()
print(rel_inst.subentity, rel_inst.otherentity)
# [<__main__.SubEntity object at 0x0000023487D42C18>] [<__main__.OtherEntity object at 0x0000023487D60278>]
The reason we can't define the related_entity() declared_attr method in BaseModel is because SQLAlchemy will not honor the cascade, and there will be no relationships generated (becuase the if has_inherited_table(cls): block prevents BaseModel from generating one). From the docs:
The flag only applies to the use of declared_attr on declarative mixin
classes and __abstract__ classes; it currently has no effect when used
on a mapped class directly.
I need to set the entity_type_id as a column value when I persist a row to a generic table of various entity_types. I should be able to load the entity_type_id for every specific instance at instantiation time because it is accessible via a simple select statement. I'd like to have that id automatically retrieved/set at the class (or instance) level without executing a query and/or manually setting to every time I persist a row of an "entity_type".
I tried an entity_type_id #property on the mixin that returns the id of the entity_type using the object_session but for reasons I don't fully understand the orm still inserts null as the entity_type_id value when I commit/flush the session. (my guess is having the "property" itself isn't the same thing as setting the attribute value on the instance and/or causing an issue because the column name from the base class has the same name)
Here's a slimmed down version of the relevant models in my schema:
class EntityType(Base):
__tablename__ = 'entity_type'
id = Column(UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
table_name = Column(String, nullable=False)
ui_label = Column(Text, unique=True, nullable=False)
entry_key = Column(Text, unique=True, nullable=False)
Base class model:
class TrackedEntity(Base):
#declared_attr
def __tablename__(cls):
return convert(cls.__name__)
__table_args__ = (
UniqueConstraint('entity_type_id', 'label'),
)
id = Column(UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
entity_type_id = Column('entity_type_id', ForeignKey('entity_type.id'))
label = Column('label', String, nullable=False)
entity_type = relationship('EntityType')
polymorphic_discriminator = column_property(select([EntityType.table_name]).where(EntityType.id == entity_type_id).as_scalar())
#declared_attr
def entity_type_label(cls):
return association_proxy('entity_type', 'label')
#declared_attr
def __mapper_args__(cls):
if cls.__name__ == 'TrackedEntity':
return {
"polymorphic_on": cls.polymorphic_discriminator,
"polymorphic_identity": cls.__tablename__
}
else:
return {"polymorphic_identity": cls.__tablename__}
Children class mixin:
class TrackedEntityMixin(object):
# noinspection PyMethodParameters
#declared_attr
def id(cls) -> Column:
return Column(ForeignKey('tracked_entity.id'), primary_key=True)
#gets me the id but isn't very helpful like this, still needs to be manually set like child.entity_type_id = child._entity_type_id
#property
def _entity_type_id(self):
return object_session(self). \
scalar(
select([EntityType.id]).
where(EntityType.table_name == self.__tablename__)
)
A child class model:
class DesignedMolecule(TrackedEntityMixin, TrackedEntity):
extra = Column('extra', String)
parents = relationship('TrackedEntity', secondary='mix_dm_parent_entity')
I have two models and one-to-one relationship between them. I would like to access columns of ItemData through Item (e.g. item = Item(); item.value). I tried to overwrote __getattr__, but this method is intensively used by SQLAlachemy base model. Any help appreciated.
class Item(Model):
__tablename__ = "item_data"
id = Column(Integer, primary_key=True)
data = relationship("ItemData", back_populates="_item", uselist=False,
foreign_keys="ItemData._item_id")
class ItemData(Model):
__tablename__ = "items_data"
id = Column(Integer, primary_key=True)
_item_id = Column(Integer, ForeignKey("items.id"))
_item = relationship("Item", back_populates="data",
foreign_keys=[_item_id])
value = Column(Integer)
I solved the problem by writing class decorator which should be apply to Item.
from sqlalchemy.ext.hybrid import hybrid_property
def direct_data_getter(cls):
'''Provide direct access to attributes of data.'''
fields = set(cls.data_cls.__mapper__.columns.keys()) - set(dir(cls))
for field in fields:
method = hybrid_property(
lambda self, field=field: getattr(self.data, field)
)
setattr(cls, field, method)
return cls
Let’s say we have several sqlalchemy models for catalogues:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer
from sqlalchemy.orm import relationship
Base = declarative_base()
class Plane(Base):
__tablename__ = 'Plane'
plane_id = Column(Integer, primary_key=True)
class Car(Base):
__tablename__ = 'Car'
car_id = Column(Integer, primary_key=True)
Now for import/export purposes we want to relate these to external ids. So for Plane we would write:
class PlaneID(Base):
issuer = Column(String(32), primary_key=True)
external_id = Column(String(16), primary_key=True)
plane_id = Column(Integer, ForeignKey(Plane.plane_id))
plane = relationship(Plane, backref='external_ids')
A CarID model would be defined in exactly the same way.
What are possibilities to automate this process?
Maybe we could use a mixin, factory, decorator or meta class. How would we generate the dynamically named Columns then? It would be good to be able to add more Columns to the generated models as needed. For example:
class CarID(ExternalID):
valid_from = Column(Date)
You can subclass DeclarativeMeta - the metaclass used in declarative_base function:
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class ExternalObject(DeclarativeMeta):
def __new__(mcs, name, bases, attributes):
if 'issuer' not in attributes:
attributes['issuer'] = Column(String(32), primary_key=True)
if 'external_id' not in attributes:
attributes['external_id'] = Column(String(16), primary_key=True)
if name[-2:] == 'ID':
ext_cls_name = name[:-2]
attr_rel = ext_cls_name.lower()
attr_id = '%s_id' % attr_rel
if attr_rel in attributes or attr_id in attributes:
# Some code here in case 'car' or 'car_id' attribute is defined in new class
pass
attributes[attr_id] = Column(Integer, ForeignKey('%s.%s' % (ext_cls_name, attr_id)))
attributes[attr_rel] = relationship(ext_cls_name, backref='external_ids')
new_cls = super().__new__(mcs, name, bases, attributes)
return new_cls
ExternalID = declarative_base(metaclass=ExternalObject)
After that you can create subclass from ExternalID and add another attributes like you did for CarID.