I am trying to integrate with a user-group-role table structure where a user can belong to many groups and have multiple roles on each group.
I found a similar question to this, however it does not allow for multiple roles: Many-to-many declarative SQLAlchemy definition for users, groups, and roles
I have the following table structure and would like to be able to access the roles in the following sort of manner: user.groups[0].roles
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(16), unique=True)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(16), unique=True)
class Group(Base):
__tablename__ = 'groups'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(16), unique=True)
class UserGroup(Base):
__tablename__ = 'user_group_role'
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
group_id = Column(Integer, ForeignKey('groups.id', ondelete='CASCADE'), nullable=False)
role_id = Column(Integer, ForeignKey('roles.id', ondelete='CASCADE'), nullable=False)
This is an example from Turbogears' default full-stack quickstart.
from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Unicode, Integer, DateTime
from sqlalchemy.orm import relation, synonym
from .model import DeclarativeBase, metadata, DBSession
# This is the association table for the many-to-many relationship between
# groups and permissions.
group_permission_table = Table('tg_group_permission', metadata,
Column('group_id', Integer,
ForeignKey('tg_group.group_id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
Column('permission_id', Integer,
ForeignKey('tg_permission.permission_id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True))
# This is the association table for the many-to-many relationship between
# groups and members - this is, the memberships.
user_group_table = Table('tg_user_group', metadata,
Column('user_id', Integer,
ForeignKey('tg_user.user_id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True),
Column('group_id', Integer,
ForeignKey('tg_group.group_id',
onupdate="CASCADE",
ondelete="CASCADE"),
primary_key=True))
class Group(DeclarativeBase):
__tablename__ = 'tg_group'
group_id = Column(Integer, autoincrement=True, primary_key=True)
group_name = Column(Unicode(16), unique=True, nullable=False)
users = relation('User', secondary=user_group_table, backref='groups')
class User(DeclarativeBase):
__tablename__ = 'tg_user'
user_id = Column(Integer, autoincrement=True, primary_key=True)
user_name = Column(Unicode(16), unique=True, nullable=False)
email_address = Column(Unicode(255), unique=True, nullable=False)
display_name = Column(Unicode(255))
class Permission(DeclarativeBase):
__tablename__ = 'tg_permission'
permission_id = Column(Integer, autoincrement=True, primary_key=True)
permission_name = Column(Unicode(63), unique=True, nullable=False)
description = Column(Unicode(255))
groups = relation(Group, secondary=group_permission_table,
backref='permissions')
Related
In my pet project I need to send some article to user and log this action with ManyToMany relationship.
So, any article can be sent to any user, but only once. After sending we put data to 'sent_log' table with what article_id sent to which user_id.
How do I get (with SQLAlchemy ORM) all articles that is not sent to given user_id?
This code doesn't work for me.
def get_user_articles(db: Session, user_telegram_id: int):
"""Get user articles matching language code and user_telegram_id."""
return (
db.query(models.Article)
.select_from(join(left=models.Article, right=models.User))
.filter(models.Article.language_code == models.User.language_code)
.filter(models.User.telegram_id == user_telegram_id)
.all()
)
Maybe I've built bad DB structure, guide me than.
My models:
sent_log = Table('sent_log', Base.metadata,
Column('user_id', ForeignKey('users.id'), primary_key=True),
Column('article_id', ForeignKey('articles.id'), primary_key=True)
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
telegram_id = Column(Integer, unique=True, index=True)
username = Column(String(50))
pet_name = Column(String(50))
language_code = Column(String(5))
sent_articles = relationship("Article", secondary=sent_log, back_populates="sent_to_user")
class Article(Base):
__tablename__ = "articles"
id = Column(Integer, primary_key=True, index=True)
text = Column(String(1024))
image_url = Column(String(500))
language_code = Column(String(255), index=True)
sent_to_user = relationship("User", secondary=sent_log, back_populates="sent_articles")
Consider the following many-to-many relationship:
class Hashes(db.Model):
id = db.Column(db.Integer, primary_key=True)
hash = db.Column(db.String, nullable=False, unique=True)
xref_hashes_users = db.Table("xref_hashes_users",
db.Column('hash', db.ForeignKey('hashes.id'), primary_key=True),
db.Column('user', db.ForeignKey('user.id'), primary_key=True))
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String)
hashes = db.relationship("Hashes", secondary="xref_hashes_users", backref="users")
Let's say I want to allow users to store some additional information about their hashes, perhaps a label. It makes sense to me that given this is a user-specific piece of information about a hash, it would go in the association table like:
xref_hashes_users = db.Table("xref_hashes_users",
db.Column('hash', db.ForeignKey('hashes.id'), primary_key=True),
db.Column('user', db.ForeignKey('user.id'), primary_key=True),
db.Column('label', db.String))
Given this schema, is it possible to use the ORM to add/remove/update labels? How would I do this?
It looks like the answer is to use an Association Object to store the extra data.
https://docs-sqlalchemy.readthedocs.io/ko/latest/orm/basic_relationships.html#association-object
So my example becomes:
class Hashes(db.Model):
id = db.Column(db.Integer, primary_key=True)
hash = db.Column(db.String, nullable=False, unique=True)
users = db.relationship("UserHashAssociation", back_populates="hashes")
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String)
hashes = db.relationship("UserHashAssociation", back_populates="users")
class UserHashAssociation(db.Model):
__tablename__ = "xref_hashes_users"
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)
hash_id = db.Column(db.Integer, db.ForeignKey("hashes.id"), primary_key=True)
label = db.Column(db.String)
users = db.relationship("User", back_populates="hashes")
hashes = db.relationship("Hashes", back_populates="users")
Then I am able to update the label like:
hash = Hashes(hash=hash_string)
user_hash_association = UserHashAssociation(label="foo", user_id=user.id)
hash.users.append(user_hash_association)
db.session.add(hash)
db.session.commit()
So, suppose there is this models:
class Country(Base):
__tablename__ = "countries"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
code = Column(String, index=True, nullable=False, unique=True)
name = Column(String, nullable=False)
class EventSource(Base):
__tablename__ = "eventsources"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
name = Column(String, nullable=False, unique=True)
class Event(Base):
__tablename__ = "events"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
country_id = Column(
UUID(as_uuid=True),
ForeignKey("countries.id", ondelete="CASCADE"),
nullable=False,
)
eventsource_id = Column(
UUID(as_uuid=True),
ForeignKey("eventsources.id", ondelete="CASCADE"),
nullable=False,
)
created_at = Column(DateTime(timezone=True), default=func.now())
And the task is to get counts of events for each country by eventsource. This is pretty easily done in raw SQL:
SELECT eventsource.name, countries.code, COUNT(events.id) as events_count
FROM events
JOIN countries ON events.country_id = countries.id
JOIN eventsources ON events.eventsource_id = eventsources.id
GROUP BY eventsources.name, countries.code;
So the result is that for each eventsource we have a count of events grouped by countries. Now the question is, how to properly setup models and make query in sqlalchemy (preferably in 2.0 style syntax) so that end result looks like a list of eventsource models where countries are relationships with aggregated events count, that can be accessed like in the last line of this next code block:
# Initialize our database:
country_en = Country(code="en", name="England")
country_de = Country(code="de", name="Germany")
eventsource_tv = EventSource(name="TV")
eventsource_internet = EventSource(name="Internet")
session.add(country_en)
session.add(country_de)
session.add(eventsource_tv)
session.add(eventsource_internet)
session.flush()
session.add(Event(country_id=country_en.id, eventsource_id = eventsource_tv.id)
session.add(Event(country_id=country_en.id, eventsource_id = eventsource_tv.id)
session.add(Event(country_id=country_en.id, eventsource_id = eventsource_tv.id)
session.add(Event(country_id=country_de.id, eventsource_id = eventsource_tv.id)
session.add(Event(country_id=country_en.id, eventsource_id = eventsource_internet.id)
session.add(Event(country_id=country_en.id, eventsource_id = eventsource_internet.id)
session.flush()
# Aggregate eventsources somehow:
eventsources = session.execute(select(EventSource).order_by(EventSource.name).all() # this is the line where some magick that solves problem should happen
# Print results. This line should output "2" (eventsource "Internet" for country "en"):
print(eventsources[0].countries[0].events_count)
For thouse who encounters the same problem, this is what I end up doing. Here is an example of relationship on a target that is a select query. My solution was to create query, and then map results to a custom class, based on the link above.
This is roughly what I've done (not exactly the code that I run, but something pretty similar):
class Country(Base):
__tablename__ = "countries"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
code = Column(String, index=True, nullable=False, unique=True)
name = Column(String, nullable=False)
class EventSource(Base):
__tablename__ = "eventsources"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
name = Column(String, nullable=False, unique=True)
class Event(Base):
__tablename__ = "events"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
country_id = Column(
UUID(as_uuid=True),
ForeignKey("countries.id", ondelete="CASCADE"),
nullable=False,
)
eventsource_id = Column(
UUID(as_uuid=True),
ForeignKey("eventsources.id", ondelete="CASCADE"),
nullable=False,
)
created_at = Column(DateTime(timezone=True), default=func.now())
country = relationship("Country")
#dataclass
class CountriesCount:
eventsource_id: UUID
country_code: str
events_count: int
events_counts_table = (
select(
Event.eventsource_id.label("eventsource_id"),
Country.code.label("country_code"),
func.count(Country.code).label("events_count"),
)
.select_from(Event)
.join(Country, Event.country)
.group_by(Event.eventsource_id, Country.code)
).alias()
EventSource.countries = relationship(
registry().map_imperatively(
CountriesCount,
events_counts_table,
primary_key=[
events_counts_table.c.eventsource_id,
events_counts_table.c.country_code,
],
),
viewonly=True,
primaryjoin=EventSource.id == events_counts_table.c.eventsource_id,
)
I'm testing my SQLAlchemy models with pytest and Factory Boy, but I find their documentation lacking in terms of relationships. I have my schema set up so there are users who can belong to multiple groups (groups can hold multiple users) and they can have multiple tokens, but a token only belongs to a single user:
_user_groups_table = Table(
'user_groups', Base.metadata,
Column('user_id', INTEGER(unsigned=True), ForeignKey('user.id')),
Column('group_id', INTEGER(unsigned=True), ForeignKey('user_group.id'))
)
class UserGroup(Base):
__tablename__ = 'user_group'
id = Column(INTEGER(unsigned=True), Sequence('user_group_id_seq'), primary_key=True, autoincrement=True)
name = Column(String(255), unique=True, nullable=False)
class User(Base):
__tablename__ = 'user'
id = Column(INTEGER(unsigned=True), Sequence('user_id_seq'), primary_key=True, autoincrement=True)
name = Column(String(255), unique=True, nullable=False)
groups = relationship('UserGroup', secondary=_user_groups_table)
auth_tokens = relationship('Token', cascade='delete')
class Token(Base):
__tablename__ = 'token'
id = Column(INTEGER(unsigned=True), Sequence('token_id_seq'), primary_key=True, autoincrement=True)
user_id = Column(INTEGER(unsigned=True), ForeignKey('user.id'), nullable=False)
value = Column(String(511), unique=True, nullable=False)
I've been trying different things, including a #factory.post_generation method that adds groups & tokens to the user instance, but when I put a user in a fixture and use it in my test functions, these fields never show up. Do you have any recommendations on how to model this schema with Factory Boy?
I am writing this for sqlalchemy part I dont know about the Factory boy. You already have a table which let you n to n relationship between users and groups(UserGroup table) and user-tokens for example:
id user_id group_id id user_id value
1 5 3 1 5 adb45
2 5 4 2 5 xyz01
3 5 5
4 1 9
5 1 3
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(255), unique=True, nullable=False)
tokens = relationship("Token", backref="user")
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(255), unique=True, nullable=False)
class UserGroup(Base):
__tablename__ = "usergroups"
id = Column(Integer, primary_key=True, autoincrement=True)
group_id = Column(Integer, ForeignKey("Group.id"))
user_id = Column(Integer, ForeignKey("User.id"))
class Token(Base):
__tablename__ = "tokens"
id = Column(Integer, primary_key=True, autoincrement=True)
value = Column(String(511), unique=True, nullable=False)
user_id = Column(Integer, ForeignKey("User.id"))
and the sqlalchemy documentation is good.
class BlogPost(SchemaBase):
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
authors = relationship('Authors', secondary='authors_to_blog_post')
__tablename__ = 'blogpost'
class Authors(SchemaBase):
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
__tablename__ = 'author'
authors_to_blog_post = Table('authors_to_blog_post', Base.metadata,
Column('author_id', Integer, ForeignKey('author.id')),
Column('blogpost_id', Integer, ForeignKey('blogpost.id'))
)
Now how to query for all blogposts without any author?
session.query(BlogPost).filter(BlogPost.authors == []) doesnt work
Found answer from here: https://groups.google.com/d/msg/sqlalchemy/Ow0bb6HvczU/VVQbtd7MnZkJ
So the solution is
session.query(BlogPost).filter(~BlogPost.authors.any())