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

如何在Prisma中查询因唯一键约束失败的已有注册记录?

竞赛注册重复提交时查询已有记录的问题

我开发了一个用户竞赛注册表单,用户提交姓名和邮箱后,注册信息会存入以contestId和clientId为复合主键的Registration表。当用户重复提交同一竞赛的注册时,Prisma会触发唯一键约束错误(错误码P2002),这符合预期,但我需要返回已存在的注册记录,却无法正确构建查询语句。

Prisma Schema 定义

model Client {
  id            Int             @id @default(autoincrement())
  email         String          @unique
  first         String?
  last          String?
  registrations Registration[]
}

model Contest {
  id            Int             @id @default(autoincrement())
  name          String          @unique
  registrations Registration[]
}

model Registration {
  contest       Contest         @relation(fields: [contestId], references: [id])
  contestId     Int
  contestant    Client          @relation(fields: [clientId], references: [id])
  clientId      Int
  points        Int

  @@id  ([contestId, clientId])
}

现有注册代码

try {
  const registration = await prisma.registration.create({
    data: {
      points: 1,
      contest: {
        connectOrCreate: {
          where: {
            name: contest,
          },
          create: {
            name: contest,
          }
        }
      },
      contestant: {
        connectOrCreate: {
          where: {
            email: email,
          },
          create: {
            first: first,
            last: last,
            email: email,
          },
        },
      },
    }
  });

  return res.status(201).send({ ...registration});
}

新用户注册正常,但重复注册会进入catch块。我认为这种先创建再捕获错误的方式比先查询是否存在更高效(重复注册概率低),也欢迎其他最佳实践建议。

失败的查询尝试

在catch块中尝试两种查询方式均失败:

第一种尝试及报错

catch (error) {
  // entry already exists
  if ('P2002' === error.code) {
    const registration = await prisma.registration.findUnique({
      where: {
        contest: {
          is: {
            name: contest,
          },
        },
        contestant: {
          is: {
            email: email,
          },
        },
      },
    })
    return res.status(200).send(...registration);
  }
  return res.status(500).end(`Register for contest error: ${error.message}`);
}

报错:

Argument where of type RegistrationWhereUniqueInput needs exactly one argument, but you provided contest and contestant.

第二种尝试及报错

const registration = await prisma.registration.findUnique({
  where: {
    contestId_clientId: {
      contest: {
        is: {
          name: contest,
        },
      },
      contestant: {
        is: {
          email: email,
        },
      },
    },
  },
})

报错:

Unknown arg `contest` in where.contestId_clientId.contest for type RegistrationContestIdClientIdCompoundUniqueInput. Did you mean `contestId`?
Unknown arg `contestant` in where.contestId_clientId.contestant for type RegistrationContestIdClientIdCompoundUniqueInput. Did you mean `contestId`?
Argument contestId for where.contestId_clientId.contestId is missing.
Argument clientId for where.contestId_clientId.clientId is missing.

我觉得使用Prisma自动生成的contestId_clientId复合主键查询是正确方向,但不知道如何通过竞赛名称和用户邮箱构建正确的查询语句?


解决方案

方法1:先获取关联ID,再用复合主键查询

复合主键contestId_clientId要求传入具体的contestId和clientId,因此需要先通过竞赛名称、用户邮箱查询对应的ID,再执行findUnique:

catch (error) {
  if ('P2002' === error.code) {
    // 并行查询竞赛和用户的ID
    const [targetContest, targetClient] = await Promise.all([
      prisma.contest.findUnique({ where: { name: contest } }),
      prisma.client.findUnique({ where: { email: email } })
    ]);

    if (targetContest && targetClient) {
      const existingRegistration = await prisma.registration.findUnique({
        where: {
          contestId_clientId: {
            contestId: targetContest.id,
            clientId: targetClient.id
          }
        },
        // 可选:关联查询竞赛和用户信息,返回更完整的数据
        include: {
          contest: true,
          contestant: true
        }
      });

      return res.status(200).send(existingRegistration);
    }

    // 理论上不会走到这里(create阶段已保证contest和client存在)
    return res.status(404).send('相关竞赛或用户记录不存在');
  }
  return res.status(500).send(`Register for contest error: ${error.message}`);
}

方法2:用findFirst直接通过关联条件查询

如果不需要严格使用findUnique,可以用findFirst结合关联模型的条件查询,更简洁:

catch (error) {
  if ('P2002' === error.code) {
    const existingRegistration = await prisma.registration.findFirst({
      where: {
        contest: { name: contest },
        contestant: { email: email }
      },
      include: {
        contest: true,
        contestant: true
      }
    });

    return existingRegistration 
      ? res.status(200).send(existingRegistration)
      : res.status(404).send('注册记录不存在');
  }
  return res.status(500).send(`Register for contest error: ${error.message}`);
}

最佳实践补充

你采用的先创建再捕获唯一键错误的方案是合理的,尤其适用于重复注册概率低的场景,避免了额外的前置查询开销。如果后续需要支持"重复提交时更新现有记录"的需求,可以考虑使用Prisma的upsert方法,但当前场景下你的方案更贴合需求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 19:40:34