You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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!
}

`;

错误原因

  1. SQL列名不匹配:Postgres表中定义的数组列为muscles_trained(下划线命名),但createMovement resolver的SQL语句中使用了musclesTrained(驼峰命名)。Postgres默认会将未加引号的标识符转为全小写,导致实际查找的列是musclestrained,与表中列名不符,插入操作失败,最终返回null,违反GraphQL的非空字段约束。
  2. 错误信息不明确: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);
  }
};

验证步骤

  1. 重启后端服务
  2. 在前端输入有效内容(例如名称:"深蹲",训练肌群:"股四头肌,臀大肌")
  3. 点击「Create Movement」按钮,数据应成功插入数据库,前端列表自动刷新显示新动作

内容的提问来源于stack exchange,提问作者julek5119

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 00:40:58