能否在Hasura的同一GraphQL事务中基于查询结果执行mutation?
Hasura 中实现事务性操作的两种方案
Hasura 完全支持事务性操作,能解决你担心的操作中断导致数据不一致的问题,以下两种方案可以实现你的需求:
方案一:使用 Hasura 批量请求(Batch Queries)
Hasura 的批量请求默认是原子性的,所有请求会在同一个 PostgreSQL 事务中执行,任意一步失败都会触发整体回滚。
重置密码场景:把「查找用户ID」和「插入重置令牌」合并为一个批量 mutation
mutation ResetPassword($email: String!, $token: String!) { # 先通过邮箱获取用户ID getUser: users(where: {email: {_eq: $email}}) { id } # 插入对应用户的重置令牌 insert_reset_tokens(objects: [{user_id: $getUser.id, token: $token, expires_at: "2024-12-31T23:59:59Z"}]) { returning { id } } }注:如果找不到对应用户,
getUser返回空数组,后续插入会自动失败,整个批量操作回滚,不会生成无效令牌。消费令牌场景:把「查找有效令牌」「修改用户密码」「删除令牌」合并为一个批量 mutation,注意要给令牌查询加行锁避免并发问题
mutation ConsumeToken($token: String!, $newPassword: String!) { # 查找有效且未过期的令牌,并加行锁防止重复消费 getToken: reset_tokens(where: {token: {_eq: $token}, expires_at: {_gt: now()}}, limit: 1) { user_id } # 修改对应用户的密码 update_users(where: {id: {_eq: $getToken.user_id}}, _set: {password: $newPassword}) { returning { id } } # 删除已使用的令牌 delete_reset_tokens(where: {token: {_eq: $token}}) { returning { id } } }只要其中任何一步失败(比如令牌已过期、用户不存在),整个事务都会回滚,不会出现密码修改了但令牌没删除的情况。
方案二:封装 PostgreSQL 自定义函数
如果你更习惯用 SQL 逻辑,把整个流程封装成 PL/pgSQL 函数,然后在 Hasura 中暴露为自定义 mutation,函数内部的所有操作天然在一个事务中执行。
示例:消费令牌的函数
CREATE OR REPLACE FUNCTION consume_reset_password_token(p_token text, p_new_password text) RETURNS BOOLEAN AS $$ DECLARE v_user_id INT; BEGIN -- 查找有效令牌并加行锁 SELECT user_id INTO v_user_id FROM reset_tokens WHERE token = p_token AND expires_at > NOW() FOR UPDATE; -- 如果令牌不存在或已过期,返回失败 IF v_user_id IS NULL THEN RETURN FALSE; END IF; -- 修改用户密码 UPDATE users SET password = p_new_password WHERE id = v_user_id; -- 删除令牌 DELETE FROM reset_tokens WHERE token = p_token; RETURN TRUE; END; $$ LANGUAGE plpgsql SECURITY DEFINER;
在 Hasura 中把这个函数添加为自定义 mutation 后,调用它时整个逻辑就是原子性的,出错自动回滚。
关键注意事项
- 批量请求中,后续操作可以引用前面查询的结果,Hasura 会自动处理依赖顺序
- 处理令牌消费时,一定要用行锁(
FOR UPDATE),防止同一令牌被多个请求同时消费导致重复修改密码 - 自定义函数要注意权限控制,比如用
SECURITY DEFINER时要确保函数执行权限安全
内容的提问来源于stack exchange,提问作者Nicolas Bouvrette
相关产品推荐
相关产品推荐

