Sqlalchemy: One to Many relationship combined with Many to Many relationship - python

I've got a User and Group table with a many to many relationship
_usergroup_table = db.Table('usergroup_table', db.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('group_id', db.Integer, db.ForeignKey('group.id')))
class User(db.Model):
"""Handles the usernames, passwords and the login status"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), nullable=False, unique=True)
class Group(db.Model):
"""Used for unix-style access control."""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), nullable=False)
users = db.relationship('User', secondary=_usergroup_table,
backref='groups')
Now i'd like to add a primary group to the user class. Of course I could just add a group_id column and a relationship to the Group class, but this has drawbacks. I'd like to get all groups when calling User.group, including primary_group. The primary group should always be part of the groups relationship.
Edit:
It seems the way to go is the association object
class User(db.Model, UserMixin):
"""Handles the usernames, passwords and the login status"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), nullable=False, unique=True)
primary_group = db.relationship(UserGroup,
primaryjoin="and_(User.id==UserGroup.user_id,UserGroup.primary==True)")
class Group(db.Model):
"""Used for unix-style access control."""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), nullable=False)
class UserGroup(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
active = db.Column(db.Boolean, default=False)
user = db.relationship(User, backref='groups', primaryjoin=(user_id==User.id))
group = db.relationship(Group, backref='users', primaryjoin=(group_id==Group.id))
I could simplify this with the AssociationProxy, but how do I force only a single primary group per user?

The group_id approach you originally thought of has several advantages here to the "boolean flag" approach.
For one thing, it is naturally constrained so that there is only one primary group per user. For another, loading user.primary_group means the ORM can identify this related row by it's primary key, and can look locally in the identity map for it, or emit a simple SELECT by primary key, instead of emitting a query that has a hard-to-index WHERE clause with a boolean inside of it. Yet another is there's no need to get into the association object pattern which simplifies the usage of the association table and allows SQLAlchemy to handle loads and updates from/to this table more efficiently.
Below we use events, including a new version (as of 0.7.7) of #validates that catches "remove" events, to ensure object-level modifications to User.groups and User.primary_group are kept in sync. (If on an older version of 0.7 you can use the attribute "remove" event or the "AttributeExtension.remove" extension method if you're still on 0.6 or earlier). If you wanted to enforce this at the DB level you could possibly use triggers to verify the integrity you're looking for:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base= declarative_base()
_usergroup_table = Table('usergroup_table', Base.metadata,
Column('user_id', Integer, ForeignKey('user.id')),
Column('group_id', Integer, ForeignKey('group.id')))
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(60), nullable=False, unique=True)
group_id = Column(Integer, ForeignKey('group.id'), nullable=False)
primary_group = relationship("Group")
#validates('primary_group')
def _add_pg(self, key, target):
self.groups.add(target)
return target
#validates('groups', include_removes=True)
def _modify_groups(self, key, target, is_remove):
if is_remove and target is self.primary_group:
del self.primary_group
return target
class Group(Base):
__tablename__ = 'group'
id = Column(Integer, primary_key=True)
name = Column(String(60), nullable=False)
users = relationship('User', secondary=_usergroup_table,
backref=backref('groups', collection_class=set))
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
g1, g2, g3 = Group(name='g1'), Group(name='g2'), Group(name='g3')
u1 = User(name='u1', primary_group=g1)
u1.groups.update([g2, g3])
s.add_all([
g1, g2, g3, u1
])
s.commit()
u1.groups.remove(g1)
assert u1.primary_group is None
u1.primary_group = g2
s.commit()

How about a GroupMemberships model to hold the association, instead of _usergroup_table? A user could have many groups through Group Memberships, and a group membership can hold additional attributes, such as whether a given Group is the associated User's primary group.
EDIT
In order to enforce a limit of one primary group per user, I would use a validation in the User model, such that any attempt to assign more (or fewer) than one primary group would result in an error when the record is saved. I am not aware of a way of achieving the same result relying purely on the database's integrity system. There are any number of ways of coding the validation check - the documentation shows a nice approach using the validates() decorator.

Related

SqlAlchemy AmbiguousForeign Keys Error - foreign key attribute is specified [duplicate]

Am trying to setup a postgresql table that has two foreign keys that point to the same primary key in another table.
When I run the script I get the error
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Company.stakeholder - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.
That is the exact error in the SQLAlchemy Documentation yet when I replicate what they have offered as a solution the error doesn't go away. What could I be doing wrong?
#The business case here is that a company can be a stakeholder in another company.
class Company(Base):
__tablename__ = 'company'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
class Stakeholder(Base):
__tablename__ = 'stakeholder'
id = Column(Integer, primary_key=True)
company_id = Column(Integer, ForeignKey('company.id'), nullable=False)
stakeholder_id = Column(Integer, ForeignKey('company.id'), nullable=False)
company = relationship("Company", foreign_keys='company_id')
stakeholder = relationship("Company", foreign_keys='stakeholder_id')
I have seen similar questions here but some of the answers recommend one uses a primaryjoin yet in the documentation it states that you don't need the primaryjoin in this situation.
Tried removing quotes from the foreign_keys and making them a list. From official documentation on Relationship Configuration: Handling Multiple Join Paths
Changed in version 0.8: relationship() can resolve ambiguity between
foreign key targets on the basis of the foreign_keys argument alone;
the primaryjoin argument is no longer needed in this situation.
Self-contained code below works with sqlalchemy>=0.9:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine(u'sqlite:///:memory:', echo=True)
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()
#The business case here is that a company can be a stakeholder in another company.
class Company(Base):
__tablename__ = 'company'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
class Stakeholder(Base):
__tablename__ = 'stakeholder'
id = Column(Integer, primary_key=True)
company_id = Column(Integer, ForeignKey('company.id'), nullable=False)
stakeholder_id = Column(Integer, ForeignKey('company.id'), nullable=False)
company = relationship("Company", foreign_keys=[company_id])
stakeholder = relationship("Company", foreign_keys=[stakeholder_id])
Base.metadata.create_all(engine)
# simple query test
q1 = session.query(Company).all()
q2 = session.query(Stakeholder).all()
The latest documentation:
http://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#handling-multiple-join-paths
The form of foreign_keys= in the documentation produces a NameError, not sure how it is expected to work when the class hasn't been created yet. With some hacking I was able to succeed with this:
company_id = Column(Integer, ForeignKey('company.id'), nullable=False)
company = relationship("Company", foreign_keys='Stakeholder.company_id')
stakeholder_id = Column(Integer, ForeignKey('company.id'), nullable=False)
stakeholder = relationship("Company",
foreign_keys='Stakeholder.stakeholder_id')
In other words:
… foreign_keys='CurrentClass.thing_id')

How to order query results for parent by related field for one-to-many relationship in SQLAlchemy

I have following DB models
class User(Base):
__tablename__ = "user"
user_id = Column("id", Integer(), primary_key=True)
groups = relationship(
"Group", back_populates="user", lazy="selectin", cascade="all, delete-orphan",
)
class Group(Base):
__tablename__ = "group"
group_id = Column("id", Integer(), primary_key=True)
user_id = Column(
Integer,
ForeignKey("user.id", ondelete="CASCADE"),
index=True,
)
division_id = Column(
String(96),
ForeignKey("division.id", onupdate="CASCADE"),
nullable=False,
)
name = Column(String(64), nullable=False)
user = relationship("User", back_populates="groups", lazy="selectin")
group = relationship("Division", back_populates="groups", lazy="selectin")
class Division(Base):
__tablename__ = "division"
division_id = Column("id", Integer, primary_key=True)
name = Column(String(64), nullable=False)
groups = relationship("Group", back_populates="group", lazy="selectin")
I want to fetch all the users ordered by their groups(can be something else as well, need to come from enduser), which I can easily achieve using the following query
session.query(User).join(Group).join(Division).order_by(Group.name).all()
And it might look that this works just fine but it doesn't, because since a user might have multiple groups, so in order to have correct result I first need to sort the groups for each user object i.e. something like sorted(User.group.order_by(Group.name) and then apply the order_by on the User model based on these sorted groups.
And the same thing can apply to division names as well. I know that we can provide default order_by fields while defining the relationship like below but that's not what I want since the order_by field need to come from enduser and can be any other field as well.
groups = relationship("Group", back_populates="user", lazy="selectin", cascade="all, delete-orphan",order_by=("Group.name"))
I can do this at data layer in python but that would not be ideal since there is already some ordering being done at DB layer.
So how can I achieve this at DB layer using SQLAlchemy or even with raw sql. Or is it even possible with sql?

Flask SQLAlchemy: adding third column to joining table

Context: I'm making an auctioning website for which I am using Flask-SQLAlchemy. My tables will need to have a many-to-many relationship (as one artpiece can have many user bids and a user can bid on many artpieces)
My question is: it is possible to add another column to my joining table to contain the id of the user bidding, the id of artpiece that they are bidding on and also how much they bid? Also if yes, how would I include this bid in the table when I add a record to said table?
bid_table = db.Table("bid_table",
db.Column("user_id", db.Integer, db.ForeignKey("user.user_id")),
db.Column("item_id", db.Integer, db.ForeignKey("artpiece.item_id"))
)
class User(db.Model):
user_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
username = db.Column(db.Integer, unique=True, nullable=False)
email = db.Column(db.String(50), unique =True, nullable=False)
password = db.Column(db.String(60), nullable=False)
creation_date = db.Column(db.DateTime, default=str(datetime.datetime.now()))
bids = db.relationship("Artpiece", secondary=bid_table, backref=db.backref("bids", lazy="dynamic"))
class Artpiece(db.Model):
item_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
artist = db.Column(db.String(40), nullable=False)
buyer = db.Column(db.String(40), nullable=False)
end_date = db.Column(db.String(40))
highest_bid = db.Column(db.String(40))
It is possible to do this with SQL Alchemy, but it's very cumbersome in my opinion.
SQLAlchemy uses a concept called an Association Proxy to turn a normal table into an association table. This table can have whatever data fields you want on it, but you have to manually tell SQLAlchemy which columns are foreign keys to the other two tables in question.
This is a good example from the documentation.
In your case, the UserKeyword table is the association proxy table that you want to build for your user/bid scenario.
The special_key column is the arbitrary data you would store like the bid amount.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
# association proxy of "user_keywords" collection
# to "keyword" attribute
keywords = association_proxy('user_keywords', 'keyword')
def __init__(self, name):
self.name = name
class UserKeyword(Base):
__tablename__ = 'user_keyword'
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
special_key = Column(String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User,
backref=backref("user_keywords",
cascade="all, delete-orphan")
)
# reference to the "Keyword" object
keyword = relationship("Keyword")
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(Base):
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
keyword = Column('keyword', String(64))
def __init__(self, keyword):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)
Check out the full documentation for instructions on how to access and create this kind of model.
Having used this in a real project, it's not particularly fun and if you can avoid it, I would recommend it.
https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html

SQLAlchemy Many to Many Understanding

I just can not wrap my head around the Many to Many Relationships in (Flask-)SQLAlchemy or how backrefs seem to apply to my problem.
Heres what I want do achieve:
n Users each have n (predefined) Assignments to do
Each User can Submit their work (Submission - belonging to one of 8 Assignments) multiple times.
Quick Example: Dummy User has 2 Assignments (e.g. Programm a For Loop), he/she submitted 2 code snippets (each graded individually) for the first assignment and none yet for the second.
So here is what I got so far in terms of Class Definition in SQLAlchemy:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
assignments = db.relationship(
"Assignment", secondary="submissions", backref=db.backref("users", lazy=True)
)
class Assignment(db.Model):
__tablename__ = "assignments"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(40), nullable=False)
class Submission(db.Model):
__tablename__ = "submissions"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
assignment_id = db.Column(db.Integer, db.ForeignKey("assignments.id"))
user = db.relationship(User, backref=db.backref("submissions"))
assignment = db.relationship(
Assignment,
backref=db.backref("submissions"),
)
What I get is these warning so I think I am missing/not understanding something here:
relationship 'Assignment.submissions' will copy column assignments.id to column submissions.assignment_id, which conflicts with relationship(s): 'Assignment.users' (copies assignments.id to submissions.assignment_id), 'User.assignments' (copies assignments.id to submissions.assignment_id).
relationship 'Submission.user' will copy column users.id to column submissions.user_id, which conflicts with relationship(s): 'Assignment.users' (copies users.id to submissions.user_id), 'User.assignments' (copies users.id to submissions.user_id) etc.
Thanks in Advance for your help!
Please read a warning section under the Association Object documentation, which describes your case where you are building separate relationships (direct to/from the association table and a secondary).
Based on your model, I assume that the many-to-many will be read-only as it will not allow you to access attributes on the "Submission" anyways, and I would mark it as such:
class User(db.Model):
__tablename__ = "users"
...
assignments = db.relationship(
"Assignment",
secondary="submissions",
backref=db.backref("users", lazy=True, viewonly=True),
viewonly=True,
)
Also Association Proxy might be useful.

Two-way foreign keys on different attributes, one-to-one and one-to-many

I have two models: User and ReferralKey, with these requirements:
On creation of User, a ReferralKey is automatically created and added to the DB
ReferralKey keeps track of which users were referred by it
ReferralKey keeps track of which user owns it
As per the answer to this question, the best solution seems to be to create the ReferralKey within the constructor of User. The solution to the other two require foreign keys, and seems really messy—entangling the tables together in such a way that I might as well put them in the same table.
The solution to the first looks like this:
def User(model):
id = Column(BigInteger(), autoincrement=True, primary_key=True)
referral_key = relationship('ReferralKey', uselist=False)
...
def __init__(self):
self.referral_key = ReferralKey()
def ReferralKey(model):
id = Column(BigInteger(), autoincrement=True, primary_key=True)
user_id = Column(BigInteger(), ForeignKey('user.id', ondelete='SET NULL'), nullable=True)
This works as intended, and solves the first and third points. The problem arises when trying to solve the 2nd. This (for some reason) necessitates a new foreign key in User, which necessitates the declaration of a relationship in both User and ReferralKey to (I guess) disambiguate the foreign keys:
def User(model):
id = Column(BigInteger(), autoincrement=True, primary_key=True)
referral_key = relationship('ReferralKey', uselist=False)
referrer_id = Column(BigInteger(), ForeignKey('referral_key.id', ondelete='SET NULL'))
referrer = relationship('ReferralKey', foreign_keys=['referrer_id'], backref='used_by')
...
def __init__(self):
self.referral_key = ReferralKey()
def ReferralKey(model):
__tablename__='referral_key'
id = Column(BigInteger(), autoincrement=True, primary_key=True)
user_id = Column(BigInteger(), ForeignKey('user.id', ondelete='SET NULL'), nullable=True)
user = relationship('User', foreign_keys=['user_id'])
I've tried all different permutations of relationship and ForeignKey, and always get the same error:
sqlalchemy.exc.CircularDependencyError: Can't sort tables for DROP; an unresolvable foreign key dependency exists between tables: referral_key, users. Please ensure that the ForeignKey and ForeignKeyConstraint objects involved in the cycle have names so that they can be dropped using DROP CONSTRAINT.
Ultimately, my problem is that I just don't understand what I'm doing. Why do I need to change the User table at all in order to keep track of things on the ReferralKey table? What purpose does the relationship declaration serve—why is it ambiguous without this declaration? If User has a foreign key referencing ReferralKey and ReferralKey has a foreign key referencing User—and either of these should be set to NULL in case of deletion, why does SQL need more information than that?
Why can't I just have:
def User(model):
id = Column(BigInteger(), autoincrement=True, primary_key=True)
def __init__(self):
ReferralKey(user_id=self.id)
def ReferralKey(model):
__tablename__='referral_key'
id = Column(BigInteger(), autoincrement=True, primary_key=True)
user_id = Column(BigInteger(), ForeignKey('user.id', ondelete='SET NULL'), nullable=True)
used_by = [list of user IDs]
def __init__(self, user_id):
if user_id:
self.user_id == user_id
This feels to me so much cleaner and more intuitive. If I want to add (or remove!) referral keys, I hardly have to worry about adding things to User because it's mostly independent of the functioning of the referral keys. Why do I need to add a column in the user table to keep track of something that I want the ReferralKey to keep track of?
I'm totally ignorant of this, basically. Would anyone mind helping me out?
Try the following.
It relies on the information:
A user creates his own master key on registration.
A user may register a slave key when he registers, but maybe not.
This way the foreign keys remain in one table but you need to differentiate between them..
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base= declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
own_referral_id = Column(Integer, ForeignKey('referral_key.id'))
own_referral_key = relationship('ReferralKey', foreign_keys=[own_referral_id], back_populates='owner')
signup_referral_id = Column(Integer, ForeignKey('referral_key.id'))
signup_referral_key = relationship('ReferralKey', foreign_keys=[signup_referral_id], back_populates='signups')
def __init__(self, **kwargs):
self.own_referral_key = ReferralKey()
super().__init__(**kwargs)
class ReferralKey(Base):
__tablename__ = "referral_key"
id = Column(Integer, primary_key=True)
owner = relationship('User', foreign_keys=[User.own_referral_id], back_populates='own_referral_key', uselist=False)
signups = relationship('User', foreign_keys=[User.signup_referral_id], back_populates='signup_referral_key', uselist=True)
e = create_engine("sqlite://")
Base.metadata.create_all(e)
s = Session(e)
u1 = User()
s.add(u1)
s.commit()
u2 = User(signup_referral_id = u1.own_referral_id)
u3 = User(signup_referral_id = u1.own_referral_id)
s.add(u2)
s.add(u3)
s.commit()
print(u1.own_referral_key.signups)

Categories

Resources