React/Postgres/GraphQL应用createMovement突变非空字段返回null错误排查
React/Postgres/GraphQL应用createMovement突变报错:Cannot return null for non-nullable field Mutation.createMovement
问题场景
构建React/Postgres/GraphQL应用时,前端执行createMovement突变时触发错误:Cannot return null for non-nullable field Mutation.createMovement,已确认数据库运行正常,但问题未解决。
相关代码
前端代码
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import { ApolloProvider } from '@apollo/client'; import client from './client'; import App from './App'; ReactDOM.render( <ApolloProvider client={client}> <App /> </ApolloProvider>, document.getElementById('root') );
App.tsx
import React, { useState } from "react"; import { useQuery, useMutation, gql } from "@apollo/client"; const GET_MOVEMENTS = gql` query { movements { id name musclesTrained } } `; const CREATE_MOVEMENT = gql` mutation CreateMovement($name: String!, $musclesTrained: [String!]!) { createMovement(name: $name, musclesTrained: $musclesTrained) { id name musclesTrained } } `; const App = () => { const { loading, error, data, refetch } = useQuery(GET_MOVEMENTS); const [createMovement] = useMutation(CREATE_MOVEMENT); const [newMovementName, setNewMovementName] = useState(""); const [newMusclesTrained, setNewMusclesTrained] = useState(""); const handleCreateMovement = async () => { try { const { data } = await createMovement({ variables: { name: newMovementName, musclesTrained: newMusclesTrained.split(","), }, }); if (data && data.createMovement) { console.log("Created movement:", data.createMovement); refetch(); } else { console.error("Failed to create movement. No data returned."); } } catch (error: any) { console.error("Error creating movement:", error.message); } }; if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <div> <h2>Create a New Movement</h2> <label> Name: <input type="text" value={newMovementName} onChange={(e) => setNewMovementName(e.target.value)} /> </label> <label> Muscles Trained (comma-separated): <input type="text" value={newMusclesTrained} onChange={(e) => setNewMusclesTrained(e.target.value)} /> </label> <button onClick={handleCreateMovement}>Create Movement</button> </div> <h2>Movements</h2> {data.movements.map((movement: any) => ( <div key={movement.id}> <h3>{movement.name}</h3> <p>Muscles Trained: {movement.musclesTrained.join(", ")}</p> </div> ))} </div> ); }; export default App;
client.ts
import { ApolloClient, InMemoryCache } from "@apollo/client"; const client = new ApolloClient({ uri: "http://localhost:4000", cache: new InMemoryCache(), }); export default client;
后端代码
database.sql
CREATE DATABASE gym_tracker; CREATE TABLE movements ( id serial PRIMARY KEY, name VARCHAR (255) NOT NULL, muscles_trained VARCHAR (255)[] NOT NULL );
db.ts
import * as pg from 'pg' const { Pool } = pg const pool = new Pool({ user: "postgres", host: "localhost", database: "gym_tracker", password: "pass", port: 5432, }); export default pool;
index.ts
import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { typeDefs } from "./schema"; import pool from "./db"; const resolvers = { Query: { movements: async () => { const result = await pool.query("SELECT * FROM movements"); return result.rows; }, movement: async (_: any, { id }: any) => { const result = await pool.query("SELECT * FROM movements WHERE id = $1", [ id, ]); return result.rows[0]; }, }, Mutation: { createMovement: async (_, { name, musclesTrained }) => { try { const result = await pool.query( "INSERT INTO movements (name, musclesTrained) VALUES ($1, $2) RETURNING *", [name, musclesTrained] ); return result.rows[0]; } catch (error) { throw new Error("Failed to create movement"); } }, }, }; const main = async () => { const server = new ApolloServer({ typeDefs, resolvers, }); await startStandaloneServer(server, { listen: { port: 4000 }, }); }; main();
schema.ts
export const typeDefs = `#graphql type Movement { id: ID! name: String! musclesTrained: [String!]! } type Exercise { id: ID! movement: Movement! repetions: Int! weight: Int! rest: Int! } type Workout { id: ID! exercises: [Exercise!]! } type Query { movements: [Movement!]! movement(id: ID!): Movement! workouts: [Workout!]! workout(id: ID!): Workout! } type Mutation { createMovement(name: String!, musclesTrained: [String!]!): Movement! updateMovement(id: ID!, name: String, musclesTrained: [String!]): Movement! deleteMovement(id: ID!): ID! } `;
错误原因
- SQL列名不匹配:Postgres表中定义的数组列为
muscles_trained(下划线命名),但createMovement resolver的SQL语句中使用了musclesTrained(驼峰命名)。Postgres默认会将未加引号的标识符转为全小写,导致实际查找的列是musclestrained,与表中列名不符,插入操作失败,最终返回null,违反GraphQL的非空字段约束。 - 错误信息不明确:resolver的catch块仅抛出通用错误,无法定位具体SQL层面的问题,增加排查难度。
修复方案
1. 修正SQL插入语句的列名
修改后端index.ts中createMovement resolver的SQL语句,将musclesTrained改为muscles_trained,同时完善错误日志:
// index.ts - Mutation.createMovement createMovement: async (_, { name, musclesTrained }) => { try { const result = await pool.query( "INSERT INTO movements (name, muscles_trained) VALUES ($1, $2) RETURNING *", [name, musclesTrained] ); return result.rows[0]; } catch (error: any) { console.error("SQL插入错误详情:", error); throw new Error(`创建动作失败: ${error.message}`); } },
2. 前端添加基础输入校验
避免空值或无效值传入后端,修改App.tsx中的handleCreateMovement方法:
// App.tsx - handleCreateMovement const handleCreateMovement = async () => { // 校验输入不为空 if (!newMovementName.trim() || !newMusclesTrained.trim()) { console.error("动作名称和训练肌群不能为空"); return; } try { const { data } = await createMovement({ variables: { name: newMovementName.trim(), // 去除每个肌群名称前后的空格 musclesTrained: newMusclesTrained.split(",").map(muscle => muscle.trim()), }, }); if (data && data.createMovement) { console.log("Created movement:", data.createMovement); refetch(); // 清空输入框 setNewMovementName(""); setNewMusclesTrained(""); } else { console.error("创建动作失败:无返回数据"); } } catch (error: any) { console.error("创建动作错误:", error.message); } };
验证步骤
- 重启后端服务
- 在前端输入有效内容(例如名称:"深蹲",训练肌群:"股四头肌,臀大肌")
- 点击「Create Movement」按钮,数据应成功插入数据库,前端列表自动刷新显示新动作
内容的提问来源于stack exchange,提问作者julek5119
相关产品推荐
相关产品推荐

