Many to many relationship with a composite key on SQLAlchemy - python

Let's say I have the following model:
class Molecule(Base):
db = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True)
data = Column(Integer)
class Atom(Base):
id = Column(Integer, primary_key=True)
weight = Column(Integer)
And I want to establish a many-to-many relationship between Molecule and Atom, what would be the best way to do it? Notice that the primary key of Molecule is composite.
Thanks

many-to-many association tables should be defined like this:
molecule2atom = Table(
'molecule2atom',
Base.metadata,
Column('molecule_db', Integer),
Column('molecule_id', Integer),
Column('atom_id', Integer, ForeignKey('atom.id')),
ForeignKeyConstraint(
('molecule_db', 'molecule_id'),
('molecule.db', 'molecule.id') ),
)
And add the relatiohship to one of the models as usual, for example, in Class Atom add:
molecules = relationship("Molecule", secondary=molecule2atom, backref="atoms")

I liked the solution given here better - composite key many to many

If you're using an association table or fully declared table metadata, you can use the primary_key=True in both columns, as suggested here.
Association table example:
employee_role = db.Table(
"employee_role",
db.Column("role_id", db.Integer, db.ForeignKey("role.id"), primary_key=True),
db.Column("employee_id", db.Integer, db.ForeignKey("agent.id"), primary_key=True),
)
Metadata example:
# this is using SQLAlchemy
class EmployeeRole(Base):
__tablename__ = "employee_role"
role_id = Column(Integer, primary_key=True)
employee_id = Column(Integer, primary_key=True)
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class EmployeeRole(db.Model):
__tablename__ = "employee_role"
role_id = db.Column(db.Integer, primary_key=True)
employee_id = db.Column(db.Integer, primary_key=True)
Alembic migration for it:
op.create_table(
'employee_role',
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('employee_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('role_id', 'employee_id')
)
SQL:
CREATE TABLE agent_role (
role_id INTEGER NOT NULL,
employee_id INTEGER NOT NULL,
PRIMARY KEY (role_id, employee_id)
);
In terms of relationship, declare it on one side (this should give you role.employees or employee.roles which should return a list):
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Employee(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
roles = db.relationship("Role", secondary=employee_role, backref="employee")
Your Role class can be:
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Role(db.Model):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(25), nullable=False, unique=True)

Related

SQLAlchemy: How to describe a many to many relationship with a bidirectional association object and composite foreign key?

I want to define a Many-to-Many Relationship with an Association Object like it's described in the SQLAlchemy Documentation. Additionally, my Project model uses a composite primary key.
I also referred to the various answers at another Stackoverflow Question and came up with this minimal example:
class User(Base):
__tablename__ = "user"
user_id = Column(String, primary_key=True)
subscriptions = relationship(
"Subscription", back_populates="user"
)
class Subscription(Base):
__tablename__ = "subscription"
__table_args__ = (
ForeignKeyConstraint(
['project_id', 'repo'],
['project.project_id', 'project.repo'],
),
)
user_id = Column(ForeignKey("user.user_id"), primary_key=True)
project_id = Column(ForeignKey("project.project_id"), primary_key=True)
repo = Column(ForeignKey("project.repo"), primary_key=True)
extra = Column(String(50))
user = relationship("User", back_populates="subscriptions")
project = relationship("Project", back_populates="subscribers", foreign_keys="[User.user_id, Project.project_id, Project.repo]")
class Project(Base):
__tablename__ = 'project'
project_id = Column(String, primary_key=True)
repo = Column(String, primary_key=True)
subscribers = relationship(
"Subscription", back_populates="project"
)
However it gives me a NoForeignKeysError at the Subscription.project relationship. What am I doing wrong here?

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?

SQLAlchemy many to many relationship with an extra column [duplicate]

I have 3 tables: User, Community, community_members (for relationship many2many of users and community).
I create this tables using Flask-SQLAlchemy:
community_members = db.Table('community_members',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('community_id', db.Integer, db.ForeignKey('community.id')),
)
class Community(db.Model):
__tablename__ = 'community'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
members = db.relationship(User, secondary=community_members,
backref=db.backref('community_members', lazy='dynamic'))
Now I want add additional field to community_members like this:
community_members = db.Table('community_members',
db.Column('id', db.Integer, primary_key=True),
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('community_id', db.Integer, db.ForeignKey('community.id')),
db.Column('time_create', db.DateTime, nullable=False, default=func.now()),
)
And now in python shell I can do this:
create community:
> c = Community()
> c.name = 'c1'
> db.session.add(c)
> db.session.commit()
add members to community:
> u1 = User.query.get(1)
> u2 = User.query.get(2)
> c.members.append(u1)
> c.members.append(u2)
> db.session.commit()
> c.members
[<User 1>, <User 2>]
Ok, this works.
But how now I can get time_create of community_members table?
You will have to switch from using a plain, many-to-many relationship to using an "Association Object", which is basically just taking the association table and giving it a proper class mapping. You'll then define one-to-many relationships to User and Community:
class Membership(db.Model):
__tablename__ = 'community_members'
id = db.Column('id', db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
time_create = db.Column(db.DateTime, nullable=False, default=func.now())
community = db.relationship(Community, backref="memberships")
user = db.relationship(User, backref="memberships")
class Community(db.Model):
__tablename__ = 'community'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
But you may only occasionally be interested in the create time; you want the old relationship back! well, you don't want to set up the relationship twice; because sqlalchemy will think that you somehow want two associations; which must mean something different! You can do this by adding in an association proxy.
from sqlalchemy.ext.associationproxy import association_proxy
Community.members = association_proxy("memberships", "user")
User.communities = association_proxy("memberships", "community")
If you only need query community_members and community table by a known user_id(such as user_id=2), In SQLAlchemy, you can perform:
session.query(community_members.c.time_create, Community.name).filter(community_members.c.user_id==2)
to get the result.

SQLAlchemy can't reflect table with primary key

These are the classes:
class User(db.Model):
__tablename__ = 'Users'
UserID = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
nullable=False
)
FirstName = db.Column(db.String(255), nullable=False)
LastName = db.Column(db.String(255), nullable=False)
Username = db.Column(db.String(255), nullable=False)
Password = db.Column(db.String(255), nullable=False)
class UserType(db.Model):
__tablename__ = 'UserTypes'
TypeID = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
nullable=False
)
Type = db.Column(db.String(255), nullable=False)
__table_args__ = (
db.CheckConstraint(
"Type IN ('Role1', 'Role2', 'Role3')"
),
)
class UserPrivilege(db.Model):
__tablename__ = 'UserPrivileges'
UserID = db.Column(db.Integer, primary_key=True)
UserTypeID = db.Column(db.Integer, primary_key=True)
__table_args__ = (
db.ForeignKeyConstraint(
['UserID'],
['Users.UserID'],
),
db.ForeignKeyConstraint(
['UserTypeID'],
['UserTypes.TypeID'],
),
)
PrivilegeUserInfoBackref = db.relationship(
'User',
backref='PrivilegeUserInfoBackref',
lazy=True,
)
PrivilegeUserTypeInfoBackref = db.relationship(
'UserType',
backref='PrivilegeUserTypeInfoBackref',
lazy=True,
)
And here is the code for reflecting the tables:
Base = automap_base()
engine = sa.create_engine(
DATABASE_CONNECTION,
convert_unicode=True,
pool_size=10,
max_overflow=20
)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base.prepare(engine, reflect=True)
The 'Users' and 'UserTypes' classes appear in Base.classes._data but for some reasson 'UserPrivileges' does not appear in Base.classes._data. All I managed to find is that tables with no primary key can't be reflected but as you can see that is not the case here. I also have some more tables that have composite primary key with backrefs but that are reflected with no problem.
So, can anyone give me any suggestions in order to reflect the last table as well, please ?
The table created for UserPrivilege ticks all the boxes of a many-to-many relationship's "secondary" table, and as such is not mapped directly when using the automap extension. This behaviour is also explained in the note of "Basic Use":
By viable, we mean that for a table to be mapped, it must specify a primary key. Additionally, if the table is detected as being a pure association table between two other tables, it will not be directly mapped and will instead be configured as a many-to-many table between the mappings for the two referring tables.
Your table should exist as Base.metadata.tables['UserPrivileges'].

sqlalchemy constraint in models inheritance

I have two simple models:
class Message(Backend.instance().get_base()):
__tablename__ = 'messages'
id = Column(Integer, primary_key=True, autoincrement=True)
sender_id = Column(Integer, ForeignKey('users.id'))
content = Column(String, nullable=False)
class ChatMessage(Message):
__tablename__ = 'chat_messages'
id = Column(Integer, ForeignKey('messages.id'), primary_key=True)
receiver_id = Column(Integer, ForeignKey('users.id'))
How to define constraint sender_id!=receiver_id?
This doesn't seem to work with joined table inheritance, I've tried and it complains that the column sender_id from Message doesn't exist when creating the constraint in ChatMessage.
This complaint makes sense, since sender_id wouldn't be in the same table as receiver_id when the tables are created, so the foreign key relationship would need to be followed to check the constraint.
One option is to make ChatMessage a single table.
Use CheckConstraint, placed in table args.
class ChatMessage(Base):
__tablename__ = 'chat_messages'
id = sa.Column(sa.Integer, primary_key=True)
sender_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
receiver_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
content = sa.Column(sa.String, nullable=False)
__table_args__ = (
sa.CheckConstraint(receiver_id != sender_id),
)

Categories

Resources