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

NestJS中Prisma原生查询转换报错(原Express+Knex查询正常)

解决NestJS+Prisma $queryRaw执行SQL时的P2010语法错误问题

问题描述

将Express+Knex环境下正常运行的SQL查询迁移到NestJS中,使用Prisma的$queryRaw执行时,抛出PrismaClientKnownRequestError错误(错误码P2010,MySQL错误码1064),提示SQL语法错误,位置靠近'?'字符。需要获取与原Express应用相同的活动详情数据。

原代码

async fetchCompanyActivities(
body: GetCompanyDetailDto,
member_id: number,
facility_id: number,
) {
const {
latitude,
longitude,
start_date,
end_date,
keyword,
activity_type_ids,
zone_ids,
} = body;
// const numberOfDaysToAdd = process.env.company_activity_no_of_days;
const numberOfDaysToAdd = 15;

const unit = 6371;
const currentDate = new Date();
const curTime =
  currentDate.getHours() +
  ':' +
  currentDate.getMinutes() +
  ':' +
  currentDate.getSeconds();

const memberQuery1 = member_id ? ` AND m0_.member_id = '${member_id}'` : '';
const memberQuery2 = member_id
  ? ` LEFT JOIN
                        (SELECT
                          schedule_detail_id ,IF(member_id = '${member_id}' , 1,0) self_booked
                          FROM
                          member_schedule_activity
                          WHERE
                          status IN ('${CompanyInfoType.STATUS_BOOKED}','${CompanyInfoType.STATUS_RESERVED}')
                          OR (STATUS = '${CompanyInfoType.STATUS_EXPIRED}' AND checkin = 1)
                          GROUP BY schedule_detail_id,member_id) m4_ ON m4_.schedule_detail_id = s3_.id `
  : '';
const memberQuery3 = member_id ? ` OR IFNULL(m4_.self_booked,1)=1` : '';
const memberQuery4 = member_id ? ` AND member_id = '${member_id}'` : '';

const distanceQuery =
  latitude && longitude
    ? `,(\n      ${unit} *\n      acos(cos(radians(${latitude})) *\n      cos(radians(IF( a2_.is_location = 0 , f1_.latitude, a2_.latitude))) *\n      cos(radians(IF( a2_.is_location = 0 , f1_.longitude, a2_.longitude)) -\n          radians(${longitude})) +\n          sin(radians(${latitude})) *\n      sin(radians(IF( a2_.is_location = 0 , f1_.latitude, a2_.latitude))))\n      ) AS distance`
    : '';

const distanceOrderByQuery =
  latitude && longitude ? `ORDER BY distance DESC` : '';

const dateFilterQuery =
  start_date && end_date
    ? `AND s3_.schedule_date >= '${start_date} ${curTime}' AND s3_.schedule_date <= '${end_date}'`
    : `AND DATE_ADD(s3_.schedule_date, INTERVAL s3_.duration MINUTE ) > \n  (SELECT _sd.schedule_date FROM schedule_detail _sd\n  INNER JOIN activity_schedule _as ON _as.id=_sd.schedule_id\n  WHERE _as.facility_id = '${facility_id}' AND _sd.schedule_date >= NOW() ORDER BY _sd.schedule_date ASC LIMIT 1) \n  AND s3_.schedule_date <= DATE_ADD(DATE_FORMAT((SELECT _sd.schedule_date FROM schedule_detail _sd\n  INNER JOIN activity_schedule _as ON _as.id=_sd.schedule_id\n  WHERE _as.facility_id = '${facility_id}' AND _sd.schedule_date >= NOW() ORDER BY _sd.schedule_date ASC LIMIT 1),'%Y-%m-%d'), INTERVAL ${numberOfDaysToAdd} DAY)`;

const keywordFilterQuery = keyword
  ? `AND (f1_.company_name LIKE '%${keyword}%' OR f1_.address LIKE '%${keyword}%' OR a2_.pinaddress LIKE '%${keyword}%' OR at_.name LIKE '%${keyword}%')`
  : '';

const activityTypeFilterQuery =
  activity_type_ids && activity_type_ids.length
    ? `AND at_.id IN (${activity_type_ids.join(',')})`
    : '';

const zoneFilterQuery =
  zone_ids && zone_ids.length
    ? `AND a2_.facilityZone_id IN (${zone_ids.join(',')})`
    : '';

const query = `SELECT DISTINCT\n                              s3_.id AS schedule_detail_id,\n                              fz.title zone,\n                              IFNULL(m3_.booked_slots,0) booked_slots,\n                              s3_.slots AS slots,\n                              s3_.schedule_id AS classId,\n                              a4_.id AS activityId,\n                              a4_.NAME AS name,\n                              at_.NAME AS actt_name,\n                              a4_.CODE AS code,\n                              a4_.description AS description,\n                              a4_.imageName AS activityImage,\n                              at_.imageName AS activityTypeImage,\n                              a2_.is_free AS isFree,\n                              a2_.offer_online AS offer_online,\n                              a2_.allow_corepass AS isCorePass,\n                              a2_.recurrence AS recurrence,\n                              s3_.duration AS duration,\n                              DATE_FORMAT(s3_.schedule_date,'%h:%i %p') AS startTime,\n                              DATE_FORMAT(DATE_ADD(s3_.schedule_date, INTERVAL s3_.duration MINUTE ), '%h:%i %p') endTime,\n                              DATE_ADD(s3_.schedule_date, INTERVAL s3_.duration MINUTE ) class_end_date_time,\n                              instrutor_.title AS instructor_name,\n                              a2_.duration AS slot_duration,\n                              a2_.facility_id AS facilityId,\n                              f1_.company_name AS facility,\n                              IF( a2_.is_location = 0 , f1_.latitude, a2_.latitude)  AS classLatitude,\n                              IF( a2_.is_location = 0 , f1_.longitude, a2_.longitude)  AS classLongitude,\n                              IF( a2_.is_location = 0 , f1_.latitude, a2_.latitude)  AS facilityLatitude,\n                              IF( a2_.is_location = 0 , f1_.longitude, a2_.longitude)  AS facilityLongitude,\n                              IF( a2_.is_location = 1 AND  LENGTH (a2_.pinaddress ) > 0 , a2_.pinaddress, f1_.address)  AS address,\n                              IF( a2_.is_location = 1 AND  LENGTH (a2_.pinaddress ) > 0 , a2_.pinaddress, f1_.address)  AS activity_schedule_address,\n                              f1_.company_logo AS facilityImage,\n                              a2_.startDate AS a_startDate,\n                              DATE_FORMAT(s3_.schedule_date,'%a,%M %D %Y') AS startDate,\n                              a2_.endDate AS endDate,\n                              a2_.updated_date AS lastUpdated,\n                              s3_.schedule_date AS classDate,\n                              m0_.is_favourite AS is_favourite_0,\n                              m0_.checkin AS checkin_1,\n                              m0_.STATUS AS status,\n                              m0_.id AS msa_id,\n                              IF( a2_.is_location = 0 , f1_.latitude, a2_.latitude)  AS latitude,\n                              IF( a2_.is_location = 0 , f1_.longitude, a2_.longitude)  AS longitude,\n                              a2_.id AS id_5,\n                              a2_.is_recommended AS is_recommended ${distanceQuery}\n                            FROM\n                              activity_schedule a2_\n                            INNER JOIN schedule_detail s3_ ON a2_.id = s3_.schedule_id\n                            INNER JOIN activity a4_ ON a2_.activity_id = a4_.id\n                            INNER JOIN facility_zones fz ON fz.id = a2_.facilityZone_id\n                            INNER JOIN activity_type at_ ON a4_.activity_type_id = at_.id \n                            INNER JOIN activity_schedule_package a6_ ON\n                                a2_.id = a6_.activityschedule_id\n                            INNER JOIN package p5_ ON\n                                p5_.id = a6_.package_id\n                            LEFT JOIN instructor instrutor_ ON\n                                        s3_.instructor_id = instrutor_.id AND(instrutor_.is_deleted = 0)\n                            INNER JOIN fos_user_user f1_ ON a2_.facility_id = f1_.id\n                            LEFT JOIN member_schedule_activity m0_ ON m0_.schedule_detail_id = s3_.id AND m0_.STATUS != '${CompanyInfoType.STATUS_CANCELLED}' ${memberQuery1}\n                            LEFT JOIN (\n                            SELECT\n                                COUNT(id) booked_slots, schedule_detail_id\n                            FROM\n                                member_schedule_activity \n                            WHERE\n                              STATUS IN ('${CompanyInfoType.STATUS_BOOKED}','${CompanyInfoType.STATUS_RESERVED}')\n                              OR (STATUS = '${CompanyInfoType.STATUS_EXPIRED}' AND checkin = 1)\n                              group by schedule_detail_id\n                            ) m3_ ON m3_.schedule_detail_id = s3_.id \n                            \n                            ${memberQuery2}\n                                \n                        WHERE\n                            a2_.is_deleted = 0 \n                            ${dateFilterQuery}  \n                            ${keywordFilterQuery}\n                            ${activityTypeFilterQuery}\n                            ${zoneFilterQuery}\n                          AND ( f1_.enabled = 1 )\n                            AND a2_.facility_id IN (${facility_id})\n                            AND s3_.is_deleted = 0 \n                            AND s3_.is_cancel = 0 \n                             AND (a2_.is_free = 1 OR (p5_.is_deleted = 0 AND ( p5_.expires_on > curdate() OR p5_.repeat_monthly = 1 ) ))\n                        GROUP BY s3_.id \n                                ORDER BY\n                             s3_.schedule_date\n                             LIMIT 100`;

const activities = await this.prismaService.$queryRaw(Prisma.sql`${query}`);
return activities;
}

错误信息

err: { 
  "type": "PrismaClientKnownRequestError", 
  "message": "\nInvalid`prisma.$queryRaw()`invocation:\n\n\nRaw query failed. Code:`1064`. Message:`You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?\n                                FROM\n                                  activit' at line 45`", 
  "stack": "Error:  Invalid `prisma.$queryRaw()` invocation:\n\n      Raw query failed. Code: `1064`. Message: `You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?\n                                      FROM\n                                        activit' at line 45`\n   at Pn.handleRequestError (.../node_modules/@prisma/client/runtime/library.js:171:6929)\n   at Pn.handleAndLogRequestError(.../node_modules/@prisma/client/runtime/library.js:171:6358)\n   at Pn.request (.../node_modules/@prisma/client/runtime/library.js:171:6237)\n          at UserRepository.fetchCompanyActivities (.../dist/apps/user/main.js:9706:28)\n          at UserService.getCompanyDetail (.../dist/apps/user/main.js:10521:19)",
  "code": "P2010",
  "clientVersion": "4.14.1",
  "meta": {
    "type": "Object",
    "message": "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?\n                                FROM\n                                  activit' at line 45",
    "code": "1064"
  }
}

问题原因

  1. 原代码直接拼接字符串生成SQL,而Prisma的Prisma.sql模板会自动对字符串中的特殊字符转义,或尝试将变量解析为参数占位符(?),导致生成的SQL语法不符合MySQL要求。
  2. 直接拼接SQL存在严重的SQL注入风险,这也是Prisma不推荐该写法的核心原因。

解决方案

改用Prisma的参数化查询方式,所有动态变量通过Prisma.value()传递,IN子句用Prisma.join()处理数组,避免直接字符串拼接。

重构后代码

async fetchCompanyActivities(
  body: GetCompanyDetailDto,
  member_id: number,
  facility_id: number,
) {
  const {
    latitude,
    longitude,
    start_date,
    end_date,
    keyword,
    activity_type_ids,
    zone_ids,
  } = body;
  const numberOfDaysToAdd = 15;
  const unit = 6371;
  const currentDate = new Date();
  const curTime = `${currentDate.getHours()}:${currentDate.getMinutes()}:${currentDate.getSeconds()}`;

  // 构建动态查询片段,使用参数化
  const memberConditions = [];
  let memberJoinFragment = Prisma.sql``;
  if (member_id) {
    memberConditions.push(Prisma.sql`AND m0_.member_id = ${Prisma.value(member_id)}`);
    memberJoinFragment = Prisma.sql`
      LEFT JOIN (
        SELECT
          schedule_detail_id, IF(member_id = ${Prisma.value(member_id)}, 1, 0) self_booked
        FROM member_schedule_activity
        WHERE
          status IN (${Prisma.value(CompanyInfoType.STATUS_BOOKED)}, ${Prisma.value(CompanyInfoType.STATUS_RESERVED)})
          OR (STATUS = ${Prisma.value(CompanyInfoType.STATUS_EXPIRED)} AND checkin = 1)
        GROUP BY schedule_detail_id, member_id
      ) m4_ ON m4_.schedule_detail_id = s3_.id
    `;
  }

  const distanceFragment = latitude && longitude
    ? Prisma.sql`
      ,(
        ${Prisma.value(unit)} *
        acos(cos(radians(${Prisma.value(latitude)})) *
        cos(radians(IF(a2_.is_location = 0, f1_.latitude, a2_.latitude))) *
        cos(radians(IF(a2_.is_location = 0, f1_.longitude, a2_.longitude)) - radians(${Prisma.value(longitude)})) +
        sin(radians(${Prisma.value(latitude)})) *
        sin(radians(IF(a2_.is_location = 0, f1_.latitude, a2_.latitude)))
      ) AS distance`
    : Prisma.sql``;

  let dateFilterFragment = Prisma.sql``;
  if (start_date && end_date) {
    const startDateTime = `${start_date} ${curTime}`;
    dateFilterFragment = Prisma.sql`
      AND s3_.schedule_date >= ${Prisma.value(startDateTime)} 
      AND s3_.schedule_date <= ${Prisma.value(end_date)}`;
  } else {
    dateFilterFragment = Prisma.sql`
      AND DATE_ADD(s3_.schedule_date, INTERVAL s3_.duration MINUTE) > (
        SELECT _sd.schedule_date FROM schedule_detail _sd
        INNER JOIN activity_schedule _as ON _as.id = _sd.schedule_id
        WHERE _as.facility_id = ${Prisma.value(facility_id)} AND _sd.schedule_date >= NOW() 
        ORDER BY _sd.schedule_date ASC LIMIT 1
      )
      AND s3_.schedule_date <= DATE_ADD(
        DATE_FORMAT(
          (SELECT _sd.schedule_date FROM schedule_detail _sd
           INNER JOIN activity_schedule _as ON _as.id = _sd.schedule_id
           WHERE _as.facility_id = ${Prisma.value(facility_id)} AND _sd.schedule_date >= NOW() 
           ORDER BY _sd.schedule_date ASC LIMIT 1), '%Y-%m-%d'
        ), INTERVAL ${Prisma.value(numberOfDaysToAdd)} DAY
      )`;
  }

  const keywordFilterFragment = keyword
    ? Prisma.sql`
      AND (
        f1_.company_name LIKE ${Prisma.value(`%${keyword}%`)} 
        OR f1_.address LIKE ${Prisma.value(`%${keyword}%`)} 
        OR a2_.pinaddress LIKE ${Prisma.value(`%${keyword}%`)} 
        OR at_.name LIKE ${Prisma.value(`%${keyword}%`)}
      )`
    : Prisma.sql``;

  const activityTypeFilterFragment = activity_type_ids?.length
    ? Prisma.sql`AND at_.id IN (${Prisma.join(activity_type_ids.map(id => Prisma.value(id)))})`
    : Prisma.sql``;

  const zoneFilterFragment = zone_ids?.length
    ? Prisma.sql`AND a2_.facilityZone_id IN (${Prisma.join(zone_ids.map(id => Prisma.value(id)))})`
    : Prisma.sql``;

  // 最终拼接完整SQL
  const query = Prisma.sql`
    SELECT DISTINCT
      s3_.id AS schedule_detail_id,
      fz.title zone,
      IFNULL(m3_.booked_slots, 0) booked_slots,
      s3_.slots AS slots,
      s3_.schedule_id AS classId,
      a4_.
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 00:06:13