SQLAlchemy通过中介表的relationship()

huangapple go评论53阅读模式
英文:

SQLAlchemy relationship() via intermediary table

问题

我在SQLAlchemy中定义方法以通过一个中介表检索相关记录方面遇到了困难。

考虑以下架构:

  • 用户可以创建多个帖子,每个帖子属于一个用户
  • 每个帖子可以有多条评论,每条评论属于一个帖子

我想要的是,对于给定的用户实例,能够检索该用户实例的所有帖子中的所有评论。

我已经这样设置了:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    id: Mapped[int] = mapped_column(primary_key=True)

# 定义模型类
class User(Base):
    __tablename__ = "users"

    name: Mapped[str] = mapped_column()

    posts: Mapped[list["Post"]] = relationship(back_populates="user")

    def __repr__(self) -> str:
        return f"(<{__class__.__name__}> name: {self.name})"


class Post(Base):
    __tablename__ = "posts"

    title: Mapped[str] = mapped_column()

    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    user: Mapped["User"] = relationship(back_populates="posts")

    comments: Mapped[list["Comment"]] = relationship(back_populates="post")

    def __repr__(self) -> str:
        return f"(<{__class__.__name__}> title: {self.title})"


class Comment(Base):
    __tablename__ = "comments"

    body: Mapped[str] = mapped_column()

    post_id: Mapped[int] = mapped_column(ForeignKey("posts.id"))
    post: Mapped["Post"] = relationship(back_populates="comments")

    def __repr__(self) -> str:
        return f"(<{__class__.__name__}> body: {self.body})"

如果我创建这些模型的几个实例,您可以看到它们之间的关系:

# 创建实例
user = User(name="greta")

post_1 = Post(title="First post", user=user)
post_2 = Post(title="Second post", user=user)

comment_1 = Comment(body="yeah wotever", post=post_1)
comment_2 = Comment(body="lol good one", post=post_1)
comment_3 = Comment(body="lmfao", post=post_2)

# 显示所有帖子及其评论
print(user)
for post in user.posts:
    print(f"   └── {post}")
    for comment in post.comments:
        print(f"          └── {comment}")
(<User> name: greta)
   └── (<Post> title: First post)
          └── (<Comment> body: yeah wotever)
          └── (<Comment> body: lol good one)
   └── (<Post> title: Second post)
          └── (<Comment> body: lmfao)

我不确定如何使用relationship()来在User类中定义一个all_comments()方法,该方法将返回该用户实例的所有帖子中的所有评论。您可以指导我吗?

英文:

I am struggling to define methods in SQLAlchemy to retrieve related records via an intermediary table.

Consider the following schema:

  • Users can create multiple posts, each post belongs to 1 user
  • Each post can have multiple comments on it, with each comment belonging to 1 post

What I want is to be able to, for a given user instance, retrieve all of the comments from all of their posts.

I have set this up as follows:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    id: Mapped[int] = mapped_column(primary_key=True)

# define model classes
class User(Base):
    __tablename__ = &quot;users&quot;

    name: Mapped[str] = mapped_column()

    posts: Mapped[list[&quot;Post&quot;]] = relationship(back_populates=&quot;user&quot;)

    def __repr__(self) -&gt; str:
        return f&quot;(&lt;{__class__.__name__}&gt; name: {self.name})&quot;


class Post(Base):
    __tablename__ = &quot;posts&quot;

    title: Mapped[str] = mapped_column()

    user_id: Mapped[int] = mapped_column(ForeignKey(&quot;users.id&quot;))
    user: Mapped[&quot;User&quot;] = relationship(back_populates=&quot;posts&quot;)

    comments: Mapped[list[&quot;Comment&quot;]] = relationship(back_populates=&quot;post&quot;)

    def __repr__(self) -&gt; str:
        return f&quot;(&lt;{__class__.__name__}&gt; title: {self.title})&quot;


class Comment(Base):
    __tablename__ = &quot;comments&quot;

    body: Mapped[str] = mapped_column()

    post_id: Mapped[int] = mapped_column(ForeignKey(&quot;posts.id&quot;))
    post: Mapped[&quot;Post&quot;] = relationship(back_populates=&quot;comments&quot;)

    def __repr__(self) -&gt; str:
        return f&quot;(&lt;{__class__.__name__}&gt; body: {self.body})&quot;

If I create a few instances of these models, you can see how things are related:

# create instances
user = User(name=&quot;greta&quot;)

post_1 = Post(title=&quot;First post&quot;, user=user)
post_2 = Post(title=&quot;Second post&quot;, user=user)

comment_1 = Comment(body=&quot;yeah wotever&quot;, post=post_1)
comment_2 = Comment(body=&quot;lol good one&quot;, post=post_1)
comment_3 = Comment(body=&quot;lmfao&quot;, post=post_2)

# show all posts, and their comments
print(user)
for post in user.posts:
    print(f&quot;   └── {post}&quot;)
    for comment in post.comments:
        print(f&quot;          └── {comment}&quot;)
(&lt;User&gt; name: greta)
   └── (&lt;Post&gt; title: First post)
          └── (&lt;Comment&gt; body: yeah wotever)
          └── (&lt;Comment&gt; body: lol good one)
   └── (&lt;Post&gt; title: Second post)
          └── (&lt;Comment&gt; body: lmfao)

I am unsure of how to use relationship() to define a method all_comments() in the User class, which would return a list of all of the comments across all of a user instance's posts.

Can anyone point me in the right direction?

答案1

得分: 1

使用你的次要表posts,你可以在relationship中使用secondaryjoinprimaryjoin来获得你想要的结果。

这样你不需要创建一个名为all_comments的方法,你可以直接使用user.comments来获取评论,同时反过来,comment.user可以获取用户信息。(请查看编辑)

你可能需要调整连接条件,从我初步的测试来看,这似乎可以满足你的要求。

我已经创建了两个不同的用户,每个用户有不同的帖子和评论,所以你可以看到区别。

from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, mapped_column, relationship, Session

# 在这里是你的类定义,省略了具体内容

engine = create_engine("sqlite:///temp.db")

# 创建数据库表
Base.metadata.create_all(engine)

# 创建会话并插入数据
with Session(engine) as session, session.begin():
    # 创建用户和相关内容的示例,省略了具体内容

# 查询并打印结果
with Session(engine) as session:
    statement = select(User)

    for user in session.scalars(statement):
        print(user, user.comments)

输出

(<User> name: greta) [(<Comment> body: yeah wotever), (<Comment> body: lol good one), (<Comment> body: lmfao)]
(<User> name: not greta) [(<Comment> body: wotever), (<Comment> body: good one)]

编辑:在这个实现中,反向关系“从评论获取用户”的部分似乎有问题,一个评论有多个用户,我不确定我哪里出错了,但如果你只想要关系“获取用户的所有评论”,那么这个方法有效。

英文:

Using your secondary table posts, you can use secondaryjoin and primaryjoin in relationship to get what you want.

This way you needn't create a method all_comments, you can just do user.comments and get the comments, <strike>also the other way round, comment.user gives you the user.</strike> (see edit)

You probably want to tweak the join conditions, from my rudimentary testing this seems to get where you want to be.

I have created two users, which different set of posts and comments so you can see the difference.

from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session


class Base(DeclarativeBase):
    id: Mapped[int] = mapped_column(primary_key=True)


class User(Base):
    __tablename__ = &quot;users&quot;

    name: Mapped[str] = mapped_column()

    posts: Mapped[list[&quot;Post&quot;]] = relationship(back_populates=&quot;user&quot;)

    comments: Mapped[list[&quot;Comment&quot;]] = relationship(
        back_populates=&quot;user&quot;,
        secondary=&quot;posts&quot;,
        primaryjoin=&quot;User.id == Post.user_id&quot;,
        secondaryjoin=&quot;Comment.post_id == Post.id&quot;,
        viewonly=True,
    )

    def __repr__(self) -&gt; str:
        return f&quot;(&lt;{__class__.__name__}&gt; name: {self.name})&quot;


class Post(Base):
    __tablename__ = &quot;posts&quot;

    title: Mapped[str] = mapped_column()

    user_id: Mapped[int] = mapped_column(ForeignKey(&quot;users.id&quot;))
    user: Mapped[&quot;User&quot;] = relationship(back_populates=&quot;posts&quot;)

    comments: Mapped[list[&quot;Comment&quot;]] = relationship(back_populates=&quot;post&quot;)

    def __repr__(self) -&gt; str:
        return f&quot;(&lt;{__class__.__name__}&gt; title: {self.title})&quot;


class Comment(Base):
    __tablename__ = &quot;comments&quot;

    body: Mapped[str] = mapped_column()

    post_id: Mapped[int] = mapped_column(ForeignKey(&quot;posts.id&quot;))
    post: Mapped[&quot;Post&quot;] = relationship(back_populates=&quot;comments&quot;)

    user: Mapped[&quot;User&quot;] = relationship(
        back_populates=&quot;comments&quot;,
        secondary=&quot;posts&quot;,
        primaryjoin=&quot;User.id == Post.user_id&quot;,
        secondaryjoin=&quot;Comment.post_id == Post.id&quot;,
        viewonly=True,
        uselist=False,
    )

    def __repr__(self) -&gt; str:
        return f&quot;(&lt;{__class__.__name__}&gt; body: {self.body})&quot;


engine = create_engine(&quot;sqlite:///temp.db&quot;)


Base.metadata.create_all(engine)

with Session(engine) as session, session.begin():
    user = User(name=&quot;greta&quot;)

    post_1 = Post(title=&quot;First post&quot;, user=user)
    post_2 = Post(title=&quot;Second post&quot;, user=user)

    comment_1 = Comment(body=&quot;yeah wotever&quot;, post=post_1)
    comment_2 = Comment(body=&quot;lol good one&quot;, post=post_1)
    comment_3 = Comment(body=&quot;lmfao&quot;, post=post_2)

    session.add_all((user, post_1, post_2, comment_1, comment_2, comment_3))

    user = User(name=&quot;not greta&quot;)

    post_1 = Post(title=&quot;Third post&quot;, user=user)
    post_2 = Post(title=&quot;Fourth post&quot;, user=user)

    comment_1 = Comment(body=&quot;wotever&quot;, post=post_1)
    comment_2 = Comment(body=&quot;good one&quot;, post=post_1)

    session.add_all((user, post_1, post_2, comment_1, comment_2))


with Session(engine) as session:
    statement = select(User)

    for user in session.scalars(statement):
        print(user, user.comments)

Output

(&lt;User&gt; name: greta) [(&lt;Comment&gt; body: yeah wotever), (&lt;Comment&gt; body: lol good one), (&lt;Comment&gt; body: lmfao)]
(&lt;User&gt; name: not greta) [(&lt;Comment&gt; body: wotever), (&lt;Comment&gt; body: good one)]

Edit: The reverse relation "get user from a comment" seems to be bugged in this implementation, one comment has more than one user, I am not sure where I went wrong, but if all you want is the relation "get all comments for a user" then this works.

huangapple
  • 本文由 发表于 2023年2月16日 08:24:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/75466701.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定