# 数据库原生查询

# 使用场景

数据模型 SDK 提供了模型化的数据操作接口,为了进一步满足用户在特定场景下的需求:

  • 利用数据库特定的功能或优化策略
  • 数据模型 SDK 尚未提供的功能
    • 对于底层为 NoSQL 云数据库类型的模型,我们推荐使用云数据库的 SDK 中的 聚合搜索事务等来完成数据模型尚未提供的功能,使用数据模型创建的 NoSQL 类型的模型,仍可以使用云数据库的所有能力。

对于底层为 MySQL 数据库类型的模型,我们提供了 MySQL 类型的原生数据库查询语句。

MySQL 数据库类型的模型,数据模型 SDK 提供两种模式的查询方法:

  • $runSQL: 预编译模式, 通过参数化查询来避免 SQL 注入风险
  • $runSQLRaw 原始模式, 更加灵活的模式,SQL 语句会当做原始字符串进行查询,存在 SQL 注入的风险

注意:

  1. runSQLrunSQLRaw 接口仅支持在服务端调用,如云函数/云托管/服务器等场景,不支持小程序/web 端直接调用
  2. 建议优先采用预编译模式,避免 SQL 注入风险
  3. 当前仅开放了 select 语句,如果有其他 SQL 语句需求,请联系我们

# 预编译模式 $runSQL

预编译模式下使用参数化查询设计,结合静态模板语法和动态运行时参数,以实现灵活的数据交互。

允许开发者通过 Mustache 变量绑定语法()直接在 SQL 查询中嵌入静态参数,同时也支持在运行时通过 $runSQL() 方法执行时动态传递参数,可以避免直接拼接字符串导致 SQL 注入的风险。

详情可参考 $runSQL 文档

例如:

  1. 查询标题为"hello"的记录

    const result = await models.$runSQL(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE title = {{title}} limit 10",
      {
        title: "hello",
      }
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"27"},"requestId":"16244844-19fe-4946-8924-d35408ced576"}
    
  2. 查询阅读次数大于 1000 的记录

    const result = await models.$runSQL(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE read_num > {{num}} limit 10",
      {
        num: 1000,
      }
    );
    
    console.log(result);
    // {"data":{"total":0,"executeResultList":[],"backendExecute":"23"},"requestId":"2f06b68f-e869-45cb-bb0d-82d50b3dcde0"}
    
  3. 查询最后更新时间在某个特定时间戳之后的记录(例如:2024-06-01 00:00:00):

    const result = await models.$runSQL(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE updatedAt > UNIX_TIMESTAMP({{timestamp}})",
      {
        timestamp: "2024-06-01 00:00:00",
      }
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"28"},"requestId":"0d4c98c3-a3ff-4c55-93cc-d0f5c835f82c"}
    
  4. 查询拥有特定 banner 图片的记录

    const result = await models.$runSQL(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE banner = '{{url}}';",
      {
        url: "cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png",
      }
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"28"},"requestId":"0d4c98c3-a3ff-4c55-93cc-d0f5c835f82c"}
    
  5. 查询作者联系电话以"1858"开头的记录

    const result = await models.$runSQL(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE author_tel LIKE '{{tel}}';",
      {
        tel: "1858%",
      }
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"28"},"requestId":"0d4c98c3-a3ff-4c55-93cc-d0f5c835f82c"}
    
  6. 查询并计算发布状态为 true 的记录数量

    const result = await models.$runSQL(
      "SELECT COUNT(*) FROM `lcap-wzcs_iuujo7p` WHERE is_published = {{isPublished}};",
      {
        isPublished: true,
      }
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"COUNT(*)":1}],"backendExecute":"1717"},"requestId":"f323d96a-8863-48db-a132-ed0fb3fbc727"}
    
  7. 查询并返回所有记录的标题和阅读次数

    const result = await models.$runSQL(
      "SELECT read_num,title FROM `lcap-wzcs_iuujo7p`"
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"read_num":997,"title":"hello"}],"backendExecute":"1916"},"requestId":"845d3fd4-05ce-4277-9a73-2cdd9b5ce04f"}
    

# 原始模式 $runSQLRaw

在某些情况(例如动态表名等)下可能希望关闭预编译模式,我们也支持直接传入原始的 SQL 语句的方式来执行 SQL,这种情况下需要自行处理 SQL 注入的防范。

详情可参考 $runSQLRaw 文档

示例:

  1. 查询标题为"hello"的记录

    const result = await models.$runSQLRaw(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE title = 'hello' limit 10"
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"27"},"requestId":"16244844-19fe-4946-8924-d35408ced576"}
    
  2. 查询阅读次数大于 1000 的记录

    const result = await models.$runSQLRaw(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE read_num > 1000 limit 10"
    );
    
    console.log(result);
    // {"data":{"total":0,"executeResultList":[],"backendExecute":"23"},"requestId":"2f06b68f-e869-45cb-bb0d-82d50b3dcde0"}
    
  3. 查询最后更新时间在某个特定时间戳之后的记录(例如:2024-06-01 00:00:00):

    const result = await models.$runSQLRaw(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE updatedAt > UNIX_TIMESTAMP('2024-06-01 00:00:00')"
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"28"},"requestId":"0d4c98c3-a3ff-4c55-93cc-d0f5c835f82c"}
    
  4. 查询拥有特定 banner 图片的记录

    const result = await models.$runSQLRaw(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE banner = 'cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png';"
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"28"},"requestId":"0d4c98c3-a3ff-4c55-93cc-d0f5c835f82c"}
    
  5. 查询作者联系电话以"1858"开头的记录

    const result = await models.$runSQLRaw(
      "SELECT * FROM `lcap-wzcs_iuujo7p` WHERE author_tel LIKE '1858%';"
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"owner":"1739272568342245378","is_published":true,"author_web":"https://qq.com","banner":"cloud://lowcode-0gr8x3i8cd1c6771.6c6f-lowcode-0gr8x3i8cd1c6771-1307578329/weda-uploader/ec687de371d4ad064efd0a424a69e969-logo (1).png","auto_no":"1000","body":"<p>hello world</p>","title":"hello","type":"[\"test\",\"test\"]","author_tel":"18588881111","createdAt":1719475245475,"createBy":"1739272568342245378","read_num":997,"updateBy":"1739272568342245378","_openid":"1739272568342245378","extra":"{}","markdown":"# aa\n\n\n\n","author_email":"a@qq.com","json":"{\"a\":\"1\"}","_id":"9JXU7BWFZJ","region":"北京市","updatedAt":1719475245475}],"backendExecute":"28"},"requestId":"0d4c98c3-a3ff-4c55-93cc-d0f5c835f82c"}
    
  6. 查询并计算发布状态为 true 的记录数量

    const result = await models.$runSQLRaw(
      "SELECT COUNT(*) FROM `lcap-wzcs_iuujo7p` WHERE is_published = TRUE;",
      {
        isPublished: true,
      }
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"COUNT(*)":1}],"backendExecute":"1717"},"requestId":"f323d96a-8863-48db-a132-ed0fb3fbc727"}
    
  7. 查询并返回所有记录的标题和阅读次数

    const result = await models.$runSQLRaw(
      "SELECT read_num,title FROM `lcap-wzcs_iuujo7p`"
    );
    
    console.log(result);
    // {"data":{"total":1,"executeResultList":[{"read_num":997,"title":"hello"}],"backendExecute":"1916"},"requestId":"845d3fd4-05ce-4277-9a73-2cdd9b5ce04f"}
    

# SQL 注入防范

在使用 $runSQLRaw 原始模式时,由于 SQL 语句会被当做原始字符串进行查询,因此开发者需要自行确保 SQL 语句的安全性,避免 SQL 注入的风险。以下是一些防范 SQL 注入的措施:

  1. 使用预编译模式:如非必要,优先使用 $runSQL 预编译模式,利用参数化查询来避免 SQL 注入。

  2. 对用户输入进行验证:在将用户输入的数据用于 SQL 语句之前,进行严格的验证和过滤,确保输入数据的合法性。

  3. 使用白名单验证:对于用户可以输入的值,使用白名单来验证,只允许预定义的安全值。

  4. 转义特殊字符:对于无法使用参数化查询的情况,确保对用户输入的数据进行转义,特别是 SQL 语句中的特殊字符,如单引号 '

  5. 错误处理:合理处理数据库查询错误,避免将详细的错误信息暴露给用户,这可能会泄露数据库结构信息,增加 SQL 注入的风险