SQLAlchemy: foreign keys to declared_attr columns - sqlalchemy

I'm having trouble using the foreign_keys argument with declared_attr columns. My models look like this:
class BasicTable(object):
created = db.Column(db.DateTime)
last_modified = db.Column(db.DateTime)
#declared_attr
def created_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_created_by_id'))
#declared_attr
def created_by(cls):
return db.relationship("AppUser", foreign_keys='{}.{}'.format(cls.__tablename__, 'created_by_id'))
#declared_attr
def last_modified_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_last_modified_by_id'))
#declared_attr
def last_modified_by(cls):
return db.relationship("AppUser", foreign_keys='{}.{}'.format(cls.__tablename__, 'last_modified_by_id'))
class AppUser(BasicTable, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64))
service_id = db.Column(db.Integer, db.ForeignKey("service.id"))
Because there are two columns in BasicTable that reference AppUser, I was getting "ambiguous foreign keys" errors, so I tried to use the foreign_keys argument as described here. The above gives me this error:
AttributeError: 'Table' object has no attribute 'last_modified_by_id'
When I check the database, that field does exist on all the tables that use BasicTable. Is this error happening because I'm referencing a declared_attr column? This suggests so, but when I tried to use the lambda technique like this:
foreign_keys=lambda: cls.created_by_id
I get this error:
InvalidRequestError: When initializing mapper Mapper|AppUser|app_user, expression 'BasicTable' failed to locate a name ("name 'BasicTable' is not defined"). If this is a class name, consider adding this relationship() to the <class 'app.models.AppUser'> class after both dependent classes have been defined.
Is there a way around this? Thanks!

class BasicTable(object):
created = db.Column(db.DateTime)
last_modified = db.Column(db.DateTime)
#declared_attr
def created_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_created_by_id'))
#declared_attr
def created_by(cls):
return db.relationship('AppUser', primaryjoin='%s.created_by_id==AppUser.id' % cls.__name__,
remote_side='AppUser.id')
#declared_attr
def last_modified_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_last_modified_by_id'))
#declared_attr
def last_modified_by(cls):
return db.relationship('AppUser', primaryjoin='%s.last_modified_by_id==AppUser.id' % cls.__name__,
remote_side='AppUser.id')

Related

SQLAlchemy - Pytest - One or more mappers failed to initialize - can't proceed with initialization of other mappers

I am trying to create a DB with user and project tables having a many-to-many relation. A user can be a part of many projects and a project can have many users. I created the models for the user, project and the association tables and am able to create the database using alembic without any issues.
However, when i try to use the models. it throws an exception
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'mapped class User->users'. Original exception was: reverse_property 'user' on relationship User.projects references relationship ProjectUsers.user, which does not reference mapper mapped class User->users
Here is the code for the models
from app.modules.database import BaseModel
from sqlalchemy import Column, Integer, String, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy_json import mutable_json_type
class Project(BaseModel):
__tablename__ = "projects"
__table_args__ = {'extend_existing': True}
project_name = Column(String, unique=True)
project_desc = Column(String)
users = relationship('app.modules.projects.project_models.ProjectUsers', back_populates='project')
owner_id = Column(Integer, ForeignKey('users.id'))
class ProjectUsers(BaseModel):
__tablename__ = "project_users"
__table_args__ = {'extend_existing': True}
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
project_id = Column(Integer, ForeignKey('projects.id'), primary_key=True)
user = relationship("app.modules.users.user_models.User", back_populates="projects")
project = relationship("app.modules.projects.project_models.Project", back_populates="users")
The User model is :
from app.modules.database import BaseModel
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from dataclasses import dataclass
#dataclass
class User(BaseModel):
__tablename__ = "users"
__table_args__ = {'extend_existing': True}
email = Column(String, unique=True)
username = Column(String, unique=True)
f_name = Column(String)
l_name = Column(String)
m_name = Column(String)
role_id = Column(Integer, ForeignKey('roles.id'))
#user_role = relationship('app.modules.roles.role_models.Role', backref="users")
# A user can have access to many projects where they are not the owners of the project.
#projects = relationship('app.modules.projects.project_models.Project', secondary="project_users", back_populates="users")
projects = relationship('app.modules.projects.project_models.ProjectUsers', back_populates='user')
def __repr__(self):
return '<User: {}>'.format(self.username)
Alembic creates the corresponding tables with the proper associations without any issues, but running a simple test like creating a new User() or Project() throws the above exception.
I think the issue might come from using a dataclass without using the correct mapping method. There are three ways to apply ORM Mappings to a dataclass. You seem to use declarative mapping so I'll just apply the declarative mapping method for SQLAlchemy 1.4.
#mapper_registry.mapped
#dataclass
class User:
__tablename__ = "users"
__table_args__ = {'extend_existing': True}
__sa_dataclass_metadata_key__ = "sa"
email: str | None = field(init=False, metadata={"sa": Column(String, unique=True)})
username: str | None = field(init=False, metadata={"sa": Column(String, unique=True)})
f_name: str | None = field(init=False, metadata={"sa": Column(String)})
l_name: str | None = field(init=False, metadata={"sa": Column(String)})
m_name: str | None = field(init=False, metadata={"sa": Column(String)})
role_id: int | None = field(init=False, metadata={"sa": Column(Integer, ForeignKey('roles.id'))})
#user_role = relationship('app.modules.roles.role_models.Role', backref="users")
# A user can have access to many projects where they are not the owners of the project.
#projects = relationship('app.modules.projects.project_models.Project', secondary="project_users", back_populates="users")
projects: list[Project] = field(default_factory=list, metadata={"sa": relationship('app.modules.projects.project_models.ProjectUsers', back_populates='user')})
# define repr by using the dataclasses.field repr kwarg
If that alone does not solve your problem, please add the definition of app.modules.database.BaseModel to your question.

In SQLAlchemy, how should I specify that the relationship field is required?

I have a model that depends on some fields on another model. This fields should be present when the record is created, but I do not see a way to enforce that on the database:
class Study(db.Model):
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
type = db.Column(Enum(StudyTypeChoices), nullable=False)
owner_id = db.Column(UUID(as_uuid=True), db.ForeignKey('owner.id'), nullable=False)
participants = db.relationship('Participant', lazy=True, cascade='save-update, merge, delete')
How can I make sure that 'participants' is provided when the Study record gets created (similar to what happens with the 'type' field)? I know I can put a wrapper around it to make sure of that, but I am wondering is there is a more neat way of doing it with sqlalchemy.
Edit: This is the definition of the Participant model
class Participant(UserBase):
id = db.Column(UUID(as_uuid=True), db.ForeignKey("user_base.id"), primary_key=True)
study_id = db.Column(UUID(as_uuid=True), db.ForeignKey('study.id'))
You can listen to before_flush events and prevent flushes containing studies without participants by raising an exception for instance.
#event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for instance in session.new: # might want to inspect session.dirty as well
if isinstance(instance, Study) and (
instance.participants is None or instance.participants == []
):
raise ValueError(
f"Study {instance} cannot have {instance.participants} participants."
)
This only checks for new studies, you might want to check in session.dirty as well for updated studies.
Full demo:
from sqlalchemy import Column, ForeignKey, Integer, create_engine, event
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
class Study(Base):
__tablename__ = "study"
id = Column(Integer, primary_key=True)
participants = relationship("Participant", uselist=True, back_populates="study")
class Participant(Base):
__tablename__ = "participant"
id = Column(Integer, primary_key=True)
study_id = Column(Integer, ForeignKey("study.id"), nullable=True)
study = relationship("Study", back_populates="participants")
#event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for instance in session.new: # might want to inspect session.dirty as well
if isinstance(instance, Study) and (
instance.participants is None or instance.participants == []
):
raise ValueError(
f"Study {instance} cannot have {instance.participants} participants."
)
engine = create_engine("sqlite://", future=True, echo=True)
Base.metadata.create_all(engine)
s1 = Study()
p1_1 = Participant()
p1_2 = Participant()
s1.participants.extend([p1_1, p1_2])
s2 = Study()
with Session(bind=engine) as session:
session.add(s1)
session.commit() # OK
with Session(bind=engine) as session:
session.add(s2)
session.commit() # ValueError

ValidationError from Composite key with marshmallow_sqlalchemy, sqlalchemy, marshmallow

I am making an API with Flask and I am using sqlalchemy/flask-sqlalchemy, marshmallow and marshmallow_sqlalchemy for handling the modeling of the database.
I am loading in the data for the Character table through the code below
character = {
'name': raw_character['name'],
'original_name': raw_character['original_name'],
'alternative_name': raw_character['alternative_name'],
}
characters_serialized.append(character)
schema = CharacterSchema()
characters = schema.load(data=characters_serialized, many=True, session=db.session)
raw_character is json as seen below:
{
"name": "Olaa",
"original_name": "olå",
"alternative_name": ["ol", "oå"]
}
The model itself is defined as a table for Character and a table representing the list of alternative names
class CharacterAlternativeName(db.Model):
__tablename__ = "character_alternative_name"
character_id = sa.Column(sa.Integer, sa.ForeignKey("character.id"), primary_key=True)
alternative_name = sa.Column(sa.String, primary_key=True)
def __repr__(self):
return "<CharacterAlternativeName(alternative_name={self.alternative_name!r})>".format(self=self)
class Character(db.Model):
__tablename__ = "character"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
original_name = sa.Column(sa.String)
alternative_name = relationship("CharacterAlternativeName")
def __repr__(self):
return "<Character(name={self.name!r})>".format(self=self)
class CharacterSchema(SQLAlchemySchema):
class Meta:
model = Character
include_relationships = True
load_instance = True # Optional: deserialize to model instances
id = auto_field()
name = auto_field()
original_name = auto_field()
alternative_name = auto_field()
The problem I am facing is that it seems to struggle to create the composite key in the CharacterAlternativeName table, as when it tries to deserialize them it gives the following error message
"marshmallow.exceptions.ValidationError: {0: {'alternative_name': {0: ["Could not deserialize related value 'ol'; expected a dictionary with keys ['character_id', 'alternative_name']"], 1: ["Could not deserialize related value 'oå'; expected a dictionary with keys ['character_id', 'alternative_name']"]}}}"
Which seems to suggest it struggles to create the composite key. Any ideas how to make the composite key work with sqlalchemy and marshmallow?

validate column of polymorphic table

I have a flask-sqlalchemy polymorphic table structure like so
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
polytype = db.Column(db.String(32), nullable=False)
value = db.Column(db.String(32))
__mapper_args__ = {'polymorphic_identity': 'parent',
'polymorphic_on': polytype}
class Child(Parent):
id = db.Column(db.Integer,db.ForeignKey('parent.id'),
primary_key=True)
#validates('value')
def validate_value(self, key, val):
# [validation code]
return value
__mapper_args__ = {'polymorphic_identity': 'child'}
and I want to validate the value field. However, the validator for Child.value, the column inherited from Parent, never runs.
What is the correct way to validate an inherited column?
There's an old open issue about it.
In that issue, it is suggested that using an event listener can work, for example:
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import event
db = SQLAlchemy()
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
polytype = db.Column(db.String(32), nullable=False)
value = db.Column(db.String(32))
__mapper_args__ = {'polymorphic_identity': 'parent',
'polymorphic_on': polytype}
class Child(Parent):
id = db.Column(db.Integer,db.ForeignKey('parent.id'),
primary_key=True)
__mapper_args__ = {'polymorphic_identity': 'child'}
#event.listens_for(Parent.value, "set", propagate=True)
def validate_value(inst, val, *args):
print(f"checking value for {inst}")
assert val == "spam"
Parent(value="spam")
Child(value="spam")
If you don't want the listener to fire on Parent instances, decorate your listener func with event.listens_for(Child.value, ...).
Another workaround for this known open issue is creating an instance method to be called inside the #validate method, for example:
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import validates
from sqlalchemy import event
db = SQLAlchemy()
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
polytype = db.Column(db.String(32), nullable=False)
value = db.Column(db.String(32))
__mapper_args__ = {'polymorphic_identity': 'parent',
'polymorphic_on': polytype}
#validates('value')
def validate_value(self, key, new_value):
self._validate_value(new_value)
def _validate_value(self, new_value):
print(f"checking value for {self}")
assert new_value == "spam"
class Child(Parent):
id = db.Column(db.Integer,db.ForeignKey('parent.id'),
primary_key=True)
__mapper_args__ = {'polymorphic_identity': 'child'}
def _validate_value(self, new_value):
print(f"checking value for {self}")
assert new_value == "child"
Parent(value="spam")
Child(value="child")
In this case, you are able to validate the value having different behaviors on the Childs overwriting the private method _validate_value

Polymorphic filtering on hybrid properties when inheriting from AbstractConcreteBase

I've had success polymorphically loading items via an AbstractConcreteBase parent. Is it possible to also filter on hybrid_property definitions of descendants? I'm also wanting to filter on different columns per child class.
Queries
# Fetching all ItemA and ItemB works well with...
ItemBase.query.all()
# Given the models below is it possible to filter on the children's
# item_id hybrid_property to fetch all ItemB with item_b_id of 1
# Result is []
ItemBase.query.filter(ItemBase.item_id == 'B1')
# This also doesn't work
# Result is everything unfiltered
ItemBase.query.filter(ItemB.item_id == 'B1')
Models:
from sqlalchemy.sql.expression import cast
from sqlalchemy.ext.declarative import AbstractConcreteBase
from sqlalchemy.ext.hybrid import hybrid_property
class ItemBase(AbstractConcreteBase, Base):
__tablename__ = None
#hybrid_property
def item_id(self): pass
#activity_id.expression
def item_id(cls):
pass
class ItemA(ItemBase):
__tablename__ = 'item_a'
__mapper_args__ = {
'polymorphic_identity': 'item_a',
'concrete':True
}
item_a_id = db.Column(db.Integer, primary_key=True)
#hybrid_property
def item_id(self):
return 'A' + str(self.item_a_id)
#activity_id.expression
def item_id(cls):
return 'A' + str(self.item_a_id)
class ItemB(ItemBase):
__tablename__ = 'item_b'
__mapper_args__ = {
'polymorphic_identity': 'item_b',
'concrete':True
}
item_b_id = db.Column(db.Integer, primary_key=True)
#hybrid_property
def item_id(self):
return 'B' + str(self.item_b_id)
#activity_id.expression
def item_id(cls):
return 'B' + str(self.item_b_id)
I'm stuck with the table structure for now. Any help is greatly appreciated.