sqlalchemy relationship select from other table instead of insert - sqlalchemy

I'm having difficulties in relationships. I have users and roles and defined model and schema for them.
the problem is when I try to add a new user with a previously defined role (I have its ID and name)it will try to update/insert the role table by the value the user specifies. but I only want to select from roles and specify that as a user role and not updating the role table(if role not found return error).
what I want to achieve is how to limit SQLalchemy in updating related tables by the value that the user specifies.
here is my models:
class User(db.Model):
"""user model
"""
__tablename__ = 'user'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False)
username = db.Column(db.String(40), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
role_id = db.Column(UUID(as_uuid=True), db.ForeignKey('role.id') , nullable=False)
class Role(db.Model):
"""role model
"""
__tablename__ = 'role'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False)
name = db.Column(db.String(40), unique=True, nullable=False)
perm_add = db.Column(db.Boolean, default=False)
perm_edit = db.Column(db.Boolean, default=False)
perm_del = db.Column(db.Boolean, default=False)
here is the schema that I defined:
class UserSchema(ma.SQLAlchemyAutoSchema):
password = ma.String(load_only=True, required=True)
email = ma.String(required=True)
role = fields.Nested("RoleSchema", only=("id", "name"), required=True)
class Meta:
model = User
sqla_session = db.session
load_instance = True
and I grab user input which is checked by schema and commit it to DB.
schema = UserSchema()
user = schema.load(request.json)
db.session.add(user)
try:
db.session.commit()
the point is here I could not change anything regarding role name or ID as it seems it is changed by schema even before applying to DB (I mean request.json)

In my example, I am using the additional webargs library. It facilitates validation on the server side and enables clean notation. Since marschmallow is based on webargs anyway, I think the addition makes sense.
I have based myself on your specifications. Depending on what you intend to do further, you may need to make adjustments.
I added a relationship to the user model to make the role easier to use.
class User(db.Model):
"""user model"""
# ...
# The role is mapped by sqlalchemy using the foreign key
# as an object and can be reached via a virtual relationship.
role = db.relationship('Role')
I have allowed the foreign key as a query parameter in the schema and limited the nested schema to the output. The email is assigned to the username.
class RoleSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Role
load_instance = True
class UserSchema(ma.SQLAlchemyAutoSchema):
# The entry of the email is converted into a username.
username = ma.String(required=True, data_key='email')
password = ma.String(required=True, load_only=True)
# The foreign key is only used here for loading.
role_id = ma.Integer(required=True, load_only=True)
# The role is dumped with a query.
role = ma.Nested("RoleSchema", only=("id", "name"), dump_only=True)
class Meta:
model = User
load_instance = True
include_relationships = True
It is now possible to query the role from the database and react if it does not exist. The database table for the roles is no longer updated automatically.
from flask import abort
from sqlalchemy.exc import SQLAlchemyError
from webargs.flaskparser import use_args, use_kwargs
# A schema-compliant input is expected as JSON
# and passed as a parameter to the function.
#blueprint.route('/users/', methods=['POST'])
#use_args(UserSchema(), location='json')
def user_new(user):
# The role is queried from the database and assigned to the user object.
# If not available, 404 Not Found is returned.
user_role = Role.query.get_or_404(user.role_id)
user.role = user_role
# Now the user can be added to the database.
db.session.add(user)
try:
db.session.commit()
except SQLAlchemyError as exc:
# If an error occurs while adding to the database,
# 422 Unprocessable Entity is returned
db.session.rollback()
abort(422)
# Upon successful completion, the new user is returned
# with the status code 201 Created.
user_schema = UserSchema()
user_data = user_schema.dump(user)
return jsonify(data=user_data), 201

Related

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

Flask-Admin: Why QuerySelectField does not work in custom edit view?

I have the following problem:
on stack Flask, Sqlalchemy, Flask-Admin created the following models:
class Store(db.Model):
id = db.Column(db.Integer, primary_key=True)
address = db.Column(db.String(200))
users = db.relationship('User', backref='store', lazy='dynamic')
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
store_id = db.Column(db.Integer, db.ForeignKey('store.id'))
name = db.Column(db.String(128))
login = db.Column(db.String(20))
password = db.Column(db.String(20))
I use the following forms and views:
class UserForm(FlaskForm):
store_id = QuerySelectField('Склад', query_factory=lambda: Store.query)
name = StringField('Name')
login = StringField('Login')
password = StringField('Password')
class AdminSet(AdminModelView):
def edit_form(self, obj=None):
form = UserForm(obj=obj)
return form
The view works without problems, but when I try to save the changes, the following error appears:
Failed to update record. (psycopg2.ProgrammingError) can't adapt type 'Store' [SQL: UPDATE "user" SET store_id=%(store_id)s WHERE "user".id = %(user_id)s] [parameters: {'store_id': <Store 2>, 'user_id': 2}] (Background on this error at: http://sqlalche.me/e/f405)
Why is this happening and what am I doing wrong?
From the WTForms documentation:
"The data property actually will store/keep an ORM model instance, not the ID."
Change your form definition to the following:
class UserForm(FlaskForm):
store = QuerySelectField('Склад', query_factory=lambda: Store.query)
name = StringField('Name')
login = StringField('Login')
password = StringField('Password')

Create a child object based on a parent one

I'm developing a DB where you at first register as a regular User and subsequently become a Driver. I'd like to create a Driver object in SQL-Alchemy based on existing User object. How can I achieve that?
Suppose that the User table already has a user with ID=3. Now we would like to add a Driver based on that user.
Here are classes that I used:
# Base class
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(MAX_NAME_LENGTH), nullable=False)
last_name = db.Column(db.String(MAX_SURNAME_LENGTH), nullable=False)
email = db.Column(db.String(MAX_EMAIL_LENGTH), nullable=False, unique=True)
# Child class
class Driver(User):
id = db.Column(db.Integer, db.ForeignKey(User.__tablename__ + '.id'), primary_key=True)
photo_url= db.Column(db.String(MAX_URL_LENGTH), nullable=False)
# ...
# we can easily add a new User
user = User(first_name='Martin', last_name='Smith', email='m.smith#gmail.com')
db.session.add(user)
db.session.commit()
# But this code will create a new user and assign it to that driver instance!
# driver = Driver(# Will require fields of both User and Driver class #)
I expect to find some method for taking parent object and extending it with child's properties. Something like:
user_from_db = db.session.query(User).filter_by(User.id == 3).first()
driver = Driver(photo_url='/your_url/', based_on=user_from_db)
# And now driver has all properties of 'user_from_db'
# Furthermore, SQL-Alchemy would create a row only in `Driver` table
How can I accomplish that using SQL-Alchemy inheritance?
First I recommend making the following changes:
The autoincrement field is to leave the task of assigning an id to the database.
For all relationship (OneToMany, ManyToMany or OneToOne) you need to specify a db.relationship in the parent class. Read this.
And __tablename__ is optional, but sometimes flask throw an error if this is not assigned.
# Base class
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
first_name = db.Column(db.String(MAX_NAME_LENGTH), nullable=False)
last_name = db.Column(db.String(MAX_SURNAME_LENGTH), nullable=False)
email = db.Column(db.String(MAX_EMAIL_LENGTH), nullable=False, unique=True)
driver = db.relationship('Driver', backref='driver', lazy=True)
# Child class
class Driver(db.Model):
__tablename__ = 'driver'
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
photo_url= db.Column(db.String(MAX_URL_LENGTH), nullable=False)
# ...
user = User(first_name='Martin', last_name='Smith', email='m.smith#gmail.com')
db.session.add(user) # first we create a user and add him to the current session
db.session.flush() # here we says that db.session.add(user) is a pending transaction. More info [here][1]
driver = Driver(id=user.id, photo_url='/your_url/')
# and we pass as driver id the id of user previously created
db.session.add(driver) # add driver to current session
db.session.commit() # commits those changes to the database.

Specifying a key for SQLAlchemy's `EncryptedType` at runtime

The SQLAlchemy-Utils documentation for the EncryptedType column type has an example that looks something like this:
secret_key = 'secretkey1234'
# setup
engine = create_engine('sqlite:///:memory:')
connection = engine.connect()
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = sa.Column(sa.Integer, primary_key=True)
username = sa.Column(EncryptedType(sa.Unicode,
secret_key,
AesEngine,
'pkcs5'))
But what if I don't know what the secret key is before I define the User class? For example, what if I want to prompt the user to enter the secret key?
This is the last example in the docs that you linked to:
The key parameter accepts a callable to allow for the key to change
per-row instead of being fixed for the whole table.
def get_key():
return 'dynamic-key'
class User(Base):
__tablename__ = 'user'
id = sa.Column(sa.Integer, primary_key=True)
username = sa.Column(EncryptedType(
sa.Unicode, get_key))

How to use factory boy to test SQLalchemy association-object models?

I'm using the SQLalchemy association-object pattern (http://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#association-object) for three model classes.
Basic relationship is on the left side one User can belong to multiple Organizations. I'm storing extra User-Organization relevant data in the association object class. Then, the association-object class maps a many-to-one to the Organization.
From SQLAlchemy point, the relationship works fine. The problem is testing this with factory boy has proven difficult and always results in error RecursionError: maximum recursion depth exceeded.
Below are the three models for the association object relationship, where User is parent and the Child is Organization:
class MemberOrgsAssoc(Model):
"""The left side of the relationship maps a User as a one-to-many to
Organizations. User-Organization relevant data is stored in
this association-object table. Then, there is a one-to-many from
this association-object table to the Organization table. """
__tablename__ = 'member_orgs'
member_id = Column(db.Integer, db.ForeignKey("users.id"), primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"), primary_key=True)
manager_id = Column(db.Integer, db.ForeignKey("users.id"))
org_title = Column(db.Unicode(50))
organization = relationship("Organization", back_populates="members")
member = relationship("User", back_populates="organizations",
foreign_keys=[member_id])
manager = relationship("User", back_populates="subordinates",
foreign_keys=[manager_id])
class User(SurrogatePK, Model):
"""A user of the app."""
__tablename__ = 'users'
username = Column(db.Unicode(80), unique=True, nullable=False)
organizations = relationship("MemberOrgsAssoc", back_populates="member",
primaryjoin = "member_orgs.c.member_id == User.id",
lazy="dynamic")
subordinates = relationship("MemberOrgsAssoc", back_populates="manager",
primaryjoin = "member_orgs.c.manager_id == User.id",
lazy="dynamic")
class Organization(SurrogatePK, Model):
"""An organization that Users may belong to."""
__tablename__ = 'organizations'
name = Column(db.Unicode(128), nullable=False)
members = relationship("MemberOrgsAssoc", back_populates="organization")
So all the above SQLAlchemy model classes and relationships seem to work as intended for now.
Below are the three factory-boy classes I'm attempting to make work.
MemberOrgs association-object factory:
class MemberOrgsAssocFactory(BaseFactory):
"""Association-object table Factory"""
class Meta:
"""Factory config"""
model = MemberOrgsAssoc
member_id = factory.SubFactory('tests.factories.UserFactory')
org_id = factory.SubFactory('tests.factories.OrganizationFactory')
manager_id = factory.SubFactory('tests.factories.UserFactory')
org_title = Sequence(lambda n: 'CEO{0}'.format(n))
organization = factory.SubFactory('tests.factories.OrganizationFactory')
member = factory.SubFactory('tests.factories.UserFactory')
manager = factory.SubFactory('tests.factories.UserFactory')
class UserFactory(BaseFactory):
"""User factory."""
class Meta:
"""Factory configuration."""
model = User
username = Sequence(lambda n: 'user{0}'.format(n))
organizations = factory.List(
[factory.SubFactory('tests.factories.MemberOrgsAssocFactory')])
subordinates = factory.List(
[factory.SubFactory('tests.factories.MemberOrgsAssocFactory')])
class OrganizationFactory(BaseFactory):
"""Company factory"""
class Meta:
"""Factory config"""
model = Organization
id = Sequence(lambda n: '{0}'.format(n))
name = Sequence(lambda n: 'company{0}'.format(n))
members = factory.List(
[factory.SubFactory('tests.factories.MemberOrgsAssocFactory')])
Finally, need to make a user for the tests and so below is a pytest fixture to make a User. This is where the tests fail due to `RecursionError: maximum recursion depth exceeded".
#pytest.fixture(scope='function')
def user(db):
"""An user for the unit tests.
setup reference: https://github.com/FactoryBoy/factory_boy/issues/101
# how to handle self referential foreign key relation in factory boy
# https://github.com/FactoryBoy/factory_boy/issues/173
"""
user = UserFactory(
organizations__0=None,
subordinates__0=None,
)
a = MemberOrgsAssocFactory(
is_org_admin=True,
is_default_org=True,
is_active=True,
)
a.organization=OrganizationFactory()
user.organizations.append(a)
db.session.commit()
return user
Error message:
E RecursionError: maximum recursion depth exceeded
!!! Recursion detected (same locals & position)
More or less resolved this, though a bit fragile overall. Must follow required pattern carefully as laid out in the sqlalchemy docs:
""" EXAMPLE USE:
# create User object, append an Organization object via association
p = User()
a = MemberOrgsAssoc(extra_data="some data")
a.organization = Organization()
p.organizations.append(a)
# iterate through Organization objects via association, including association attributes:
for assoc in p.organizations:
print(assoc.extra_data)
print(assoc.child)
"""
Below changes to the pytest fixture resolved the RecursionError issue and got it working:
#pytest.fixture(scope='function')
def user(db):
"""An user for the tests."""
user = UserFactory(
organizations='',
subordinates=''
)
a = MemberOrgsAssocFactory(
member_id=None,
org_id=None,
manager_id=None,
is_org_admin=True,
is_default_org=True,
is_active=True,
organization=None,
member=None,
manager=None
)
a.organization = OrganizationFactory(members=[])
user.organizations.append(a)
db.session.commit()
# debugging
# thisuser = User.get_by_id(user.id)
# for assoc in thisuser.organizations:
# if assoc.is_default_org:
# print('The default organization of thisuser is -> {}'.format(assoc.organization.name))
return user