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

在Supabase GraphQL中实现分类关联产品查询的问题求助

问题描述

我在Supabase中拥有两张表:

  • category:id (int4 主键), name (text)
  • product:id (int4 主键), name (text), price (int8), category_id (外键关联category的id)

我希望在resolver函数中编写查询语句,以获取分类及其所有关联的产品。尝试使用.join方法,但Supabase提示该方法不存在。以下是我的相关代码:

category.schema.js

module.exports = `
  type Category {
    id: Int
    name: String!
    product:Product!
  }
  type Query {
    getCategoryList: [category_product]!
    getCategory(id: Int!): Category
  }
  type Mutation {
    addCategory(name: String!): Category
    updateCategory(id:ID!,name: String!): Category
    deleteCategory(id:ID!): Category
  }
`;

product.schema.js

module.exports = `
type Product {
  id: Int
  name: String!
  price: Int!
  isDeleted: Boolean!
  category: Category!
}
  type Query {
    getProductList: [Product]
    getProduct(id: Int!): Product
  }
  type Mutation {
    addProduct(name: String!, price: Int!, category: Int!): Product
    updateProduct(id: Int!,name:String,price:Int!): Product
    deleteProduct(id:ID!): Product
  }
`;

category.resolver.js

const { isEmpty } = require("lodash");
const supabase = require("../../supabase");

let validator = {};

module.exports = {
  Query: {
    getCategoryList: async () => {
     try {
      const {data,error} = await supabase.from("category").select(`id,name,product(id,name,price)`);
      if (error) {
        throw new Error(error.message);
      }
      console.log("data :",data);
      return data;
     } catch (error) {
      console.log("Error",error);
      return null;
     }
    },
    getCategory: async (parent, args) => {
      const id = args.id;
      if (!/^[0-9a-fA-F]{24}$/.test(id)) {
        validator.error = ERROR_MSG.INVALID_ID;
        throw new ValidationException(validator);
      }
      const result = category
        .findById(id)
        .populate([
          {
            path: "productId",
            select: "name price isDeleted _id",
          },
        ])
        .select("_id name productId");
      if (!result) {
        validator.category = ERROR_MSG.CATEGORY_ERROR;
        throw new ValidationException(validator);
      }
      return result;
    },
  },
  Mutation: {
    addCategory: async (parent, args) => {
      const name = args.name;
      if (!/^[a-zA-Z ]+$/.test(name)) {
        validator.name = WARNING_MSG.CATEGORY_NAME_WARNING;
      }
      const categotyExists = await category.findOne({ name });
      if (categotyExists) {
        validator.category = WARNING_MSG.CATEGORY_WARNING;
      }
      if (!isEmpty(validator)) {
        throw new ValidationException(validator);
      } else {
        let categories = await category.create({
          name: args.name,
        });
        return { categories };
      }
    },
    updateCategory: async (parent, args) => {
      const { id, name } = args;
      const categotyExists = await category.findOne({ name });
      if (categotyExists) {
        validator.error = WARNING_MSG.CATEGORY_WARNING;
      } else {
        if (/^[0-9a-fA-F]{24}$/.test(id)) {
          const result = category.findByIdAndUpdate(
            id,
            {
              name: args.name,
            },
            { new: true }
          );
          return result;
        } else {
          validator.error = ERROR_MSG.CATEGORY_ERROR;
        }
      }
      if (!isEmpty(validator)) {
        throw new ValidationException(validator);
      }
    },
    deleteCategory: async (parent, args) => {
      const id = args.id;
      if (id) {
        const result = product.findByIdAndUpdate(
          id,
          {
            isDeleted: true,
          },
          { new: true }
        );
        return result;
      }
    },
  },
};

解决方案

1. 核心问题说明

Supabase基于PostgREST,不支持.join方法,关联查询需通过嵌套select语法或GraphQL类型解析器实现。同时你的代码混合了MongoDB语法(如findById、populate),需全部替换为Supabase API。

2. 修正GraphQL Schema

category.schema.js(修正字段类型与返回值)

module.exports = `
  type Category {
    id: Int
    name: String!
    products: [Product!]! # 一个分类对应多个产品,改为数组类型
  }
  type Query {
    getCategoryList: [Category!]! # 修正返回类型为合法的Category数组
    getCategory(id: Int!): Category
  }
  type Mutation {
    addCategory(name: String!): Category
    updateCategory(id: ID!, name: String!): Category
    deleteCategory(id: ID!): Category
  }
`;

product.schema.js(修正外键参数名)

module.exports = `
type Product {
  id: Int
  name: String!
  price: Int!
  isDeleted: Boolean!
  category: Category!
}
  type Query {
    getProductList: [Product]
    getProduct(id: Int!): Product
  }
  type Mutation {
    addProduct(name: String!, price: Int!, category_id: Int!): Product # 参数名与数据库外键一致
    updateProduct(id: Int!, name: String, price: Int!): Product
    deleteProduct(id: ID!): Product
  }
`;

3. 完整修正后的category.resolver.js

const { isEmpty } = require("lodash");
const supabase = require("../../supabase");

let validator = {};
// 补充定义缺失的常量与异常类
const ERROR_MSG = {
  INVALID_ID: "无效的ID格式",
  CATEGORY_ERROR: "分类不存在"
};
const WARNING_MSG = {
  CATEGORY_NAME_WARNING: "分类名称只能包含字母和空格",
  CATEGORY_WARNING: "该分类已存在"
};
class ValidationException extends Error {
  constructor(validator) {
    super("验证错误");
    this.validator = validator;
  }
}

module.exports = {
  // 解析Category类型的products关联字段
  Category: {
    products: async (parent) => {
      const { data, error } = await supabase
        .from("product")
        .select("id, name, price, isDeleted")
        .eq("category_id", parent.id);
      if (error) throw new Error(error.message);
      return data;
    }
  },
  Query: {
    getCategoryList: async () => {
      try {
        const { data, error } = await supabase.from("category").select("id, name");
        if (error) throw new Error(error.message);
        return data;
      } catch (error) {
        console.log("Error", error);
        return null;
      }
    },
    getCategory: async (parent, args) => {
      const id = args.id;
      validator = {};
      // Supabase使用int类型ID,无需校验MongoDB ObjectID格式
      if (isNaN(id) || id <= 0) {
        validator.error = ERROR_MSG.INVALID_ID;
        throw new ValidationException(validator);
      }
      try {
        const { data, error } = await supabase
          .from("category")
          .select("id, name")
          .eq("id", id)
          .single();
        if (error) {
          validator.category = ERROR_MSG.CATEGORY_ERROR;
          throw new ValidationException(validator);
        }
        return data;
      } catch (error) {
        console.log("Error", error);
        throw error;
      }
    },
  },
  Mutation: {
    addCategory: async (parent, args) => {
      const name = args.name;
      validator = {};
      if (!/^[a-zA-Z ]+$/.test(name)) {
        validator.name = WARNING_MSG.CATEGORY_NAME_WARNING;
      }
      const { data: categoryExists } = await supabase
        .from("category")
        .select("id")
        .eq("name", name)
        .single();
      if (categoryExists) {
        validator.category = WARNING_MSG.CATEGORY_WARNING;
      }
      if (!isEmpty(validator)) {
        throw new ValidationException(validator);
      }
      const { data, error } = await supabase
        .from("category")
        .insert({ name })
        .select()
        .single();
      if (error) throw new Error(error.message);
      return data;
    },
    updateCategory: async (parent, args) => {
      const { id, name } = args;
      validator = {};
      if (isNaN(id) || id <= 0) {
        validator.error = ERROR_MSG.INVALID_ID;
        throw new ValidationException(validator);
      }
      const { data: categoryExists } = await supabase
        .from("category")
        .select("id")
        .eq("name", name)
        .neq("id", id)
        .single();
      if (categoryExists) {
        validator.error = WARNING_MSG.CATEGORY_WARNING;
      }
      if (!isEmpty(validator)) {
        throw new ValidationException(validator);
      }
      const { data, error } = await supabase
        .from("category")
        .update({ name })
        .eq("id", id)
        .select()
        .single();
      if (error) throw new Error(error.message);
      return data;
    },
    deleteCategory: async (parent, args) => {
      const id = args.id;
      validator = {};
      if (isNaN(id) || id <= 0) {
        validator.error = ERROR_MSG.INVALID_ID;
        throw new ValidationException(validator);
      }
      // 若需保留关联产品,可先将product的category_id设为null,再删除分类
      const { data, error } = await supabase
        .from("category")
        .delete()
        .eq("id", id)
        .select()
        .single();
      if (error) throw new Error(error.message);
      return data;
    },
  },
};

4. 关键注意点

  • Supabase关联查询:可通过select('id,name,product(*)')直接嵌套查询,或通过类型解析器单独查询关联数据(后者更灵活)
  • 字段一致性:GraphQL Schema字段需与数据库表字段、Resolver返回值类型匹配
  • 移除MongoDB语法:所有findById、populate等MongoDB操作需替换为Supabase的from、select、eq等API

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 15:04:56