news 2026/9/25 6:54:09

在 Django + Graphene 中实现 Hackernews 的 Links 与 Votes 关联:从外键建模到投票 Mutation 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Django + Graphene 中实现 Hackernews 的 Links 与 Votes 关联:从外键建模到投票 Mutation 实战

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

本教程基于 howtographql 仓库的 graphql-python 系列 中的核心章节,讲解在 Django 与 Graphene 搭建的 GraphQL 服务器中,如何把用户(User)挂接到链接(Link)上、为链接添加投票(Vote)功能,并打通三者之间的查询关系。读完本文,你将掌握:通过settings.AUTH_USER_MODEL建立模型外键、在 Graphene Mutation 中读写当前登录用户、构造带权限校验的CreateVote投票 Mutation,以及用DjangoObjectType暴露关联查询。

该章节承接 用户认证章节(使用django-graphql-jwt实现 Token 认证),也为此后章节中的 过滤、分页 与 Relay 重构 打好了数据模型基础。

一、整体思路:Hackernews 的两大核心功能

在系列教程的 Getting Started 章节中已经明确,这个 Hackernews 克隆需要具备三大能力:用户与链接列表、用户创建与认证、用户创建链接并为其投票。本章解决的正是最后一项:

  • 让Link记录是谁发布的(posted_by外键指向用户);
  • 新增Vote模型,记录"哪个用户投了哪个链接";
  • 提供CreateLink(带作者)与CreateVote两个 Mutation;
  • 通过VoteType与votes字段,让客户端能查询到投票数据及每个链接的关联投票。

在此之前,你已经完成了 基础查询(LinkType、links查询)、基础 Mutation(CreateLink)与 用户认证(usersapp、UserType、tokenAuth等 JWT Mutation)。本章就是把这些能力串起来:让链接归属到用户,让用户能对链接投票。

二、把用户挂到链接上:posted_by外键

1. 修改模型:引入 Django 内置用户模型

认证章节里,我们通过django.contrib.auth的get_user_model()创建了用户。要把链接与用户关联,最规范的方式不是硬编码自定义用户模型名,而是引用settings.AUTH_USER_MODEL,这样后续即使替换了自定义用户模型,代码也无需改动。

在links/models.py顶部导入 Django 设置:

from django.conf import settings # ...code

然后在Link模型末尾追加posted_by字段:

posted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)

这里有两个参数值得注意:

  • null=True:允许posted_by为空。这意味着匿名(未登录)用户创建链接时,该字段可存NULL,不会因为缺少用户而报错;
  • on_delete=models.CASCADE:当关联的用户被删除时,其名下所有链接一并删除,保持数据一致性(Django 2.x 之后on_delete为必填参数)。

2. 同步数据库

模型变更后,需要让 Django 生成迁移并应用到数据库:

python manage.py makemigrations python manage.py migrate

第一条命令扫描模型变化生成迁移文件,第二条命令将其写入数据库(本系列使用开箱即用的 SQLite)。这与 Queries 章节 中创建Link模型后的流程完全一致。

3. 改造CreateLink:返回发布者

接下来修改links/schema.py。先导入认证章节定义的UserType:

from users.schema import UserType

然后把CreateLink改为带posted_by输出的完整版本:

class CreateLink(graphene.Mutation): id = graphene.Int() url = graphene.String() description = graphene.String() posted_by = graphene.Field(UserType) class Arguments: url = graphene.String() description = graphene.String() def mutate(self, info, url, description): user = info.context.user or None link = Link( url=url, description=description, posted_by=user, ) link.save() return CreateLink( id=link.id, url=link.url, description=link.description, posted_by=link.posted_by, )

对比 Mutations 章节 中的原始版本,核心变化有两处:

  1. 输出字段新增posted_by = graphene.Field(UserType),客户端可以像查询User类型一样自由选择username、email等字段——这正是 认证章节 中强调的"返回完整对象而非逐字段拼接";
  2. mutate方法通过info.context.user读取当前请求的用户。这里的info是 GraphQL 解析上下文,info.context.user由django-graphql-jwt的JSONWebTokenMiddleware在认证章节的配置中注入:请求携带合法 Token 时它就是当前登录用户;未携带时则为AnonymousUser。or None保证匿名用户场景下posted_by写入NULL,与模型上的null=True呼应。

测试时记得带上 Token:在 Insomnia 的Header选项卡中添加AUTHORIZATION请求头,值为JWT <token>(详见 认证章节的测试部分),再发送createLinkMutation。若一切正常,返回结果中会包含postedBy对象,展示当前用户的字段信息。

三、添加投票:Vote模型与CreateVoteMutation

Hackernews 的核心玩法是投票:链接被投得越多就越热门。投票涉及两个主体——用户和链接,因此需要一张关联表。

1. 定义Vote模型

在links/models.py中追加:

class Vote(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) link = models.ForeignKey('links.Link', related_name='votes', on_delete=models.CASCADE)

解读:

  • user外键指向用户模型,on_delete=CASCADE表示用户删除时投票记录一并删除;
  • link外键指向links.Link(注意这里使用字符串引用'links.Link'也是 Django 支持的写法),related_name='votes'非常关键——它定义了反向关系名,使得Link对象可以通过link.votes访问其全部投票记录,后续查询章节会用到。

同样执行数据库同步:

python manage.py makemigrations python manage.py migrate

2. 定义CreateVoteMutation

在links/schema.py中,先把模型导入改为同时引入Vote:

from links.models import Link, Vote

然后添加投票 Mutation:

class CreateVote(graphene.Mutation): user = graphene.Field(UserType) link = graphene.Field(LinkType) class Arguments: link_id = graphene.Int() def mutate(self, info, link_id): user = info.context.user if user.is_anonymous: raise Exception('You must be logged to vote!') link = Link.objects.filter(id=link_id).first() if not link: raise Exception('Invalid Link!') Vote.objects.create( user=user, link=link, ) return CreateVote(user=user, link=link)

最后把新 Mutation 注册到根 Mutation 类:

class Mutation(graphene.ObjectType): create_link = CreateLink.Field() create_vote = CreateVote.Field()

这个 Mutation 展示了 GraphQL 服务端的典型"三步校验"模式:

步骤代码作用
身份校验user.is_anonymous判断匿名用户禁止投票,抛出'You must be logged to vote!'
数据校验Link.objects.filter(id=link_id).first()使用first()而非get(),链接不存在时得到None而非抛DoesNotExist,随后抛出'Invalid Link!'
落库Vote.objects.create(...)创建投票记录并返回CreateVote(user=user, link=link)作为输出

两种异常在当前章节中都通过raise Exception('message')抛出;在后续的 Error Handling 章节 中,其中一条会被替换为from graphql import GraphQLError的raise GraphQLError(...)——两者都会中止执行并把括号内的消息返回给客户端,效果等价。

测试时对第一个链接发起createVoteMutation(携带 Token),成功后会返回投票者用户对象与被投票的链接对象。

四、打通查询:VoteType与votes字段

投票能写入,但还看不到——此时需要把投票数据暴露到查询层。

1. 定义VoteType

在links/schema.py中,紧跟LinkType之后添加:

class VoteType(DjangoObjectType): class Meta: model = Vote

DjangoObjectType是 Graphene-Django 提供的桥接类型,会根据 Django 模型自动生成对应的 GraphQL 字段。由于Vote模型上的user与link都是外键,VoteType会天然暴露user和link两个关联对象字段,客户端可以嵌套查询"谁投了哪个链接"。

2. 扩展 Query:新增votes字段与解析器

class Query(graphene.ObjectType): links = graphene.List(LinkType) votes = graphene.List(VoteType) def resolve_links(self, info, **kwargs): return Link.objects.all() def resolve_votes(self, info, **kwargs): return Vote.objects.all()

resolve_votes返回Vote.objects.all(),即所有投票记录。查询示例:

query { votes { id user { username } link { id url } } }

3. 反向关联:从链接看投票

因为模型中设置了related_name='votes',客户端还可以从链接一侧反向查询:

query { links { id url votes { id user { username } } } }

这就是"Relating Links and Votes"一节要达成的目标:LinkType借助 Django ORM 的反向关系(link.votes)自动获得votes字段,无需再手写解析器。Graphene-Django 会为模型上的每个关系自动生成可查询字段,这种"模型即类型"的映射正是DjangoObjectType的核心价值。

五、本章在完整教程中的位置与延伸

投票与关联打通后,本系列后续章节都是在此基础上继续演进:

  • Error Handling:把CreateVote中的raise Exception('You must be logged to vote!')换成raise GraphQLError(...),并演示对无效链接投票时的错误响应;
  • Filtering:Query在links/votes基础上增加search参数,通过django.db.models.Q实现url__icontains与description__icontains的模糊搜索;
  • Pagination:为links查询增加first/skip参数,用 Python 切片对查询集分页;
  • Relay:在schema_relay.py中为Link/Vote建立LinkNode/VoteNode,并用DjangoFilterConnectionField与graphene.relay.ClientIDMutation重构本节的查询与投票功能。

可以看到,本章设计的Vote模型、VoteType与"外键 + related_name"的关联方式,是整个教程数据层的中枢:过滤、分页、Relay 章节都直接复用了这里的模型结构与查询字段。

六、小结:本节的三个关键设计

  1. 用settings.AUTH_USER_MODEL而非硬编码用户类,保证用户模型可替换性;null=True让匿名创建的链接合法存在。
  2. Mutation 的校验顺序:先验身份(is_anonymous),再验数据(first()判空),最后落库返回完整对象——这个模式可复用于几乎所有需要用户态的写操作。
  3. DjangoObjectType+related_name自动打通关联查询:VoteType暴露双向关系,客户端既可查"所有投票",也可从link.votes反向看每个链接的投票列表,全程无需手写关联解析器。

完成本节后,你的 Hackernews GraphQL 服务器已经具备完整的"发布—投票—查询"闭环。继续阅读 Error Handling 章节,学习如何用GraphQLError让错误信息更加规范。

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

相关推荐

上一篇:React-Paginate:轻量级的React分页组件
下一篇:告别繁琐验证:validate.js 让前端表单校验如丝般顺滑

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 6:50:33

FT2232H+MPSSE:手把手搭出USB转JTAG调试链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 6:48:09

AMD平台RL训练bitwise一致:RL-Kernel与vime实战解析

如果你问我&#xff0c;在 RL 训练系统里最容易藏雷的地方是哪里&#xff0c;我不会先说分布式采样、内存泄漏或者梯度过大&#xff0c;而是“训练和 rollout 在数值上差了最后几个 bit”。尤其是当训练侧跑在 RL-Kernel 这类自研内核上&#xff0c;环境侧跑在 vime 这类模拟执…

作者头像 李华
网站建设 2026/9/25 6:48:02

Atlas 300V 24G推理卡实战:YOLO模型从PyTorch到OM的完整部署

前阵子帮一个客户做智能质检方案选型&#xff0c;对方手里压着一张 Atlas 300V 24G 卡&#xff0c;开口第一句就问&#xff1a;“这卡到底是运算加速卡吗&#xff1f;能不能直接把 YOLO 跑起来&#xff1f;”我当时就发现&#xff0c;这个疑问其实非常普遍——很多人第一次接触…

作者头像 李华
网站建设 2026/9/25 6:45:19

MiniMax H3本地部署实战:ComfyUI视频生成工作流搭建与调优

1. 为什么要在本地跑 MiniMax H3&#xff1a;从云端排队到桌面工作流的思路转变第一次听说 MiniMax H3 能出片的时候&#xff0c;我其实是持怀疑态度的。那会儿我还在用在线平台跑视频生成&#xff0c;每天盯着进度条&#xff0c;高峰期排个二十分钟是常事&#xff0c;生成一条…

作者头像 李华