SQLAlchemy lazy=dynamic with m2m relationship using association object pattern - python

I have a simple m2m relationship between users and roles tables:
users_roles = db.Table('users_roles',
db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
db.Column('role_id', db.Integer, db.ForeignKey('roles.id')),
db.Column('is_primary', db.Boolean)
)
class User(db.Model):
__tablename__ = 'users'
id = db.Column('id', db.Integer, primary_key=True)
roles = db.relationship('Role', secondary=users_roles, lazy='dynamic', backref=db.backref('users', lazy='dynamic'))
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column('id', db.Integer, primary_key=True)
users = db.relationship('User', secondary=users_roles, lazy='dynamic', backref=db.backref('roles', lazy='dynamic'))
To add a record to the users_roles table, I have to do something like this:
role = Role.get(1)
user = User()
user.roles.append(role)
db.session.add(user)
db.session.commit()
That is okay, but I have a column named is_primary in the users_roles table that should also be populated.
I changed my code to use the Association Object Pattern as described in the SQLAlchemy documentation.
Now my code looks like this:
class User(db.Model):
__tablename__ = 'users'
id = db.Column('id', db.Integer, primary_key=True)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column('id', db.Integer, primary_key=True)
class UserRole(db.Model):
__tablename__ = 'users_roles'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), primary_key=True)
is_primary = db.Column(db.Boolean)
user = db.relationship(User, backref="users_roles")
role = db.relationship(Role, backref="users_roles")
User.roles = association_proxy("users_roles", "role")
Role.users = association_proxy("users_roles", "user")
It works nice, but I still have a problem.
Is it possible that User.roles (added with the association proxy) returns an AppenderBaseQuery that I can add more filters, e.g. User.query.get(1).roles.filter_by(...)?
I was used to do that with the plain many-to-many relationship using lazy=dynamic in the relationship declaration, but after giving a class mapping to the association table it seems that I cannot do it anymore.
Is there a way to achieve that?
#IfLoop I followed your recommendation in this post. Your help would be much appreciated.

Well, I ended up filtering roles using the following code:
roles = Role.query.filter_by(...).join(UserRole).join(User).filter_by(id=1)
I still want to be able to do something like this:
roles = User.query.get(1).roles.filter_by(...).all()
Anyway if I get no answers in a few days I will accept this as an answer.

Way too late for helping you but I asked myself the same question, and this paragraph of the docs shed some light on what is possible to do from proxy associations:
https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html#querying-with-association-proxies
In summary from what I understood: this is not explicitly possible but For association proxies where the immediate target is a related object or collection, relationship-oriented operators can be used instead, such as .has() and .any()
I'm not sure this will help me, but I'm laying this there if it can ever point someone to their solution

Related

How do I associate two foreign keys to separate instances of the same parent model in SQLAlchemy?

I am working on a user to user 'Challenge' model in SQLAlchemy that needs to be linked to both the sending player, as well as the receiving player. I.e one user sends a challenge to another user, and users can see all of their sent challenges along with their received challenges. I initially attempted to solve this using an association table with no luck. I have since realized that an association table is unnecessary, however, I am unable to join the two tables as desired without receiving this error: Could not determine join condition between parent/child tables on relationship User.sent_challenges - there are multiple foreign key paths linking the tables.
I have read through the documentation and all similar problems that I could find through these forums but none seem to fix my problem. Below is the current implementation of my code. It is far from the only attempt I have made, however, I believe that it most accurately portrays what I am attempting to accomplish.
Challenge model:
class Challenge(db.Model):
__tablename__ = 'challenges'
id = Column(Integer, primary_key=True)
time = Column(Integer)
player_1_score = Column(Integer, nullable=True)
player_2_score = Column(Integer, nullable=True)
sender_id = Column(Integer, ForeignKey('users.id'), nullable=False)
receiver_id = Column(Integer, ForeignKey('users.id'), nullable=False)
sender = relationship("User", back_populates="sent_challenges")
receiver = relationship("User", back_populates="received_challenges")
User model:
class User(db.Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
user_name = Column(String(14), nullable=False)
level = Column(Integer)
high_score = Column(Integer)
points = Column(Integer)
sent_challenges = relationship("Challenge", back_populates="sender", cascade="all, delete")
received_challenges = relationship("Challenge", back_populates="receiver", cascade="all, delete")
Any insight would be greatly appreciated.
Add the foreign_keys param so sqlalchemy knows which foreign key belongs to which relationship:
sender = relationship("User", foreign_keys=[sender_id], back_populates="sent_challenges")
receiver = relationship("User", foreign_keys=[receiver_id], back_populates="received_challenges")
This is explained here with addresses that mirror your sender/receiver ambiguity:
handling-multiple-join-paths

Appropriate use of server_default=FetchedValue()

Checking some code in the backend, found some uses of server_default=FetchedValue() in Flask using SQLAlchemy, the use of it would vary depending the case.
For instance, use in non primary key will cause the columns will remain empty and will be fetched during first use when executing SELECT statement as documented:
__tablename__ = 'dummy_type'
dummy_type_code = db.Column(db.String, primary_key=True)
dummy_ind = db.Column(db.Boolean, nullable=False, server_default=FetchedValue())
__tablename__ = 'dummy_recommendation'
dummy_recommendation_id = db.Column(
db.Integer, primary_key=True, server_default=FetchedValue())
dummy_recommendation_guid = db.Column(
UUID(as_uuid=True), nullable=False, server_default=FetchedValue())
In other cases, the server_default=FetchedValue() can be applied in the PK:
__tablename__ = 'dummy_note'
dummy_note_guid = db.Column(
UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
It is not clear to me what is the appropriate use on these examples. In the first examples using the server_default, what is the advantage to generate the value until first use? Am I missing something? The use in PK is not clear to me at all.
Can you please clarify the use?
Thanks

sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers

This error happened when I tried to get access to the page. I didn't get errors when I created the tables, but seems like there are problems still.
The models are like this:
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
sell_items = db.relationship('Item', backref='user')
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
item_name = db.Column(db.String(64), index=True)
item_image = db.Column(db.String(200), index=True)
price = db.Column(db.Float(10), index=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('User', backref='sell_items')
The whole error message is this
Triggering mapper: 'Mapper|User|user'. Original exception was: Error creating backref 'user' on relationship 'User.sell_items': property of that name exists on mapper 'Mapper|Item|item'
How can I fix this? What I want to do is to refer to username who sells the item, but I cannot. There is a problem with the relationships between the models.
When you use backref the backwards relationship is automatically created, so it should only be used in one side of the relationship. In your case, you can remove the sell_items in the User model and the User model will automatically get a relationship from Item.
To declare the relationshiop on both sides (in case you want to customize its name, for example, use back_populates='name_of_relationship_on_other_model'.
in your Item class, replace this line
user = db.relationship('User', backref='sell_items')
with this line
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
it should work that way, from there you can query like this item = Item.query.first(), then item.sell_items... to get the user who posted the item.
i hope it helps.

alembic doesn't detect relationship table

I've defined some models with some association tables for m2m relationships:
from itsdangerous import TimedJSONWebSignatureSerializer
from passlib.hash import bcrypt
from sqlalchemy.ext.declarative import declarative_base
import app
from app import db
Base = declarative_base()
class UserGroupRelationship(Base):
__tablename__ = 'users_groups'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey('groups.id'), primary_key=True)
class FriendRelationship(Base):
__tablename__ = u'users_friends'
id = db.Column(db.Integer, primary_key=True)
user_left = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
user_right = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
class User(db.Model):
__tablename__ = u'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
email = db.Column(db.String(120), unique=True)
password = db.Column(db.String(120))
# ...
last_login = db.Column(db.DateTime, default=db.func.now())
friends = db.relationship(FriendRelationship,
primaryjoin=id==FriendRelationship.user_left,
backref='friends', lazy='dynamic')
friends_with = db.relationship(FriendRelationship,
primaryjoin=id==FriendRelationship.user_right,
backref='friends_with', lazy='dynamic')
class Group(db.Model):
__tablename__ = u'groups'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
users = db.relationship(UserGroupRelationship,
primaryjoin=id==UserGroupRelationship.group_id,
backref='groups', lazy='dynamic')
class Device(db.Model):
''' devices linked to users '''
__tablename__ = u'devices'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
uuid = db.Column(db.String(50))
date_added = db.Column(db.DateTime)
user_id = db.Column(db.Integer, db.ForeignKey('groups.id'))
running alembic revision --autogenerate does generate table for classes inheriting from db.Model but not for the tables used for my m2m relationships.
INFO [alembic.migration] Context impl PostgresqlImpl.
INFO [alembic.migration] Will assume transactional DDL.
INFO [alembic.autogenerate] Detected added table u'groups'
INFO [alembic.autogenerate] Detected added table u'users'
INFO [alembic.autogenerate] Detected added table u'devices'
INFO [alembic.autogenerate] Detected added table u'question'
Generating /Users/rxdazn/w/xxx/xxx-
yyy/migrations/versions/4e47aa7f3050_.py...done
My alembic.ini and env.py files are the default ones. I simply import my models init my project's __init__.py
any idea of what could cause this behaviour?
(Miguel answered this in the comments. I'll delete this answer if he posts it and someone pokes me in a comment. I only posted it so it could be marked as an answer, as I had the same problem and almost left the page before reading the comments.)
Don't inherit the association tables from Base. All models should inherit from db.Model, like this:
class FriendRelationship(db.Model):
This is why:
There are two patterns for many-to-many relationships. The basic one
uses a Table object for the association table. The advanced one uses
models, and has the advantage that you can store additional columns in
the association table. You seem to be using the advanced one, but if
you are using Flask-SQLAlchemy then all your models should inherit
from db.Model. You should not go directly to SQLAlchemy.

What's the proper way to describe an associative object by SQLalchemy the declarative way

I'm looking for a way to describe an associative object the declarative way. Beyond storing the foreign keys in the association table, I need to store information like the creation date of the association.
Today, my model looks like that :
# Define the User class
class User(Base):
__tablename__ = 'users'
# Define User fields
id = schema.Column(types.Integer(unsigned=True),
schema.Sequence('users_seq_id', optional=True), primary_key=True)
password = schema.Column(types.Unicode(64), nullable=False)
# Define the UserSubset class
class UserSubset(Base):
__tablename__ = 'subsets'
# Define UserSubset fields
id = schema.Column(types.Integer(unsigned=True),
schema.Sequence('subsets_seq_id', optional=True), primary_key=True)
some_short_description = schema.Column(types.Unicode(50), nullable=False)
# Define the subset memberships table
subset_memberships = schema.Table('group_memberships', Base.metadata,
schema.Column('user_id', types.Integer(unsigned=True), ForeignKey('users.id')),
schema.Column('subset_id', types.Integer(unsigned=True), ForeignKey('subsets.id')),
schema.Column('created', types.DateTime(), default=now, nullable=False),
)
Can I connect everything in an associative object ? Or should I change stop using the declarative way ?
What you are using at the moment is just a Many-to-Many-relation. How to work with association objects is described in the docs.
There is also an extension called associationproxy which simplifies the relation.
As you can see in the manual, configuring a one to many relation is really simple:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
addresses = relation("Address", backref="user")
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
email = Column(String(50))
user_id = Column(Integer, ForeignKey('users.id'))
Many to many relations isn't much harder:
There’s nothing special about many-to-many with declarative. The secondary argument to relation() still requires a Table object, not a declarative class. The Table should share the same MetaData object used by the declarative base:
keywords = Table('keywords', Base.metadata,
Column('author_id', Integer, ForeignKey('authors.id')),
Column('keyword_id', Integer, ForeignKey('keywords.id'))
)
class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
keywords = relation("Keyword", secondary=keywords)
You should generally not map a class and also specify its table in a many-to-many relation, since the ORM may issue duplicate INSERT and DELETE statements.
Anyway, what you seem to be doing might be better served with inheritance. Of course, there can be complex table relations that will be a pathological case for the declarative way, but this doesn't seem to be one of them.
One more thing, code comments should state what the following code does ans why, not how it does it. Having a # Define the User class comment is almost like having a line of code saying a = 1 # assing value 1 to variable "a".

Categories

Resources