1. 从零到一为什么我们需要一个像 ToolJet 这样的内部工具平台如果你在任何一个超过10人的技术团队待过大概率都经历过这样的场景业务部门提了一个需求比如“需要一个简单的后台能让我们运营同学查看和审核用户上传的内容”或者“销售团队想要一个仪表盘实时显示本月各区域的业绩数据”。对于开发团队来说这些需求技术难度不高但优先级往往排不上号。结果就是要么业务同学苦等排期要么开发同学在繁忙的迭代中挤出时间用最“快糙猛”的方式——比如写个简陋的脚本、或者用 Flask/Django 快速搭个只有增删改查的页面——应付过去。这种临时工具代码质量堪忧没有文档部署随意过几个月可能连当初写的人都忘了怎么维护。这就是“内部工具”的典型困境它们对业务至关重要但投入产出比ROI在传统的开发流程中显得很低。每个工具都像是一个“一次性”项目重复造轮子技术栈不统一维护成本却在无形中累加。我经历过一个团队为了各种内部需求维护着超过20个这样的小型应用技术栈从 Python、Node.js 到 PHP 都有部署方式五花八门光是记住各个应用的访问地址和登录密码就够头疼的。ToolJet 的出现正是为了解决这个痛点。它本质上是一个低代码/无代码的应用构建平台但它的目标用户非常明确开发者和有一定技术背景的业务人员如数据分析师、产品经理。它不是一个试图取代专业开发的“玩具”而是一个旨在提升开发效率、统一内部工具技术栈和部署规范的“生产力平台”。你可以把它理解为一个高度可视化、可拖拽的“React 后端服务”生成器。它提供了丰富的预制UI组件表格、图表、表单等和超过80种数据源连接器从 PostgreSQL、MySQL 到 Stripe、Slack、S3让你可以通过配置和少量的自定义代码快速搭建出功能完整、界面美观的内部应用。我最初接触 ToolJet 是抱着怀疑态度的毕竟低代码平台很多但真正能在生产环境扛起大梁的寥寥无几。但在深度使用并基于它构建了数个内部管理系统后我的看法彻底改变了。它最吸引我的核心价值在于在提供可视化便捷性的同时没有牺牲开发的灵活性和控制权。你仍然可以在任何需要的地方插入 JavaScript 或 Python 代码可以自定义组件样式可以连接自建的私有数据库。这种“开箱即用”与“深度定制”的结合让它从一个简单的工具变成了一个可以承载严肃业务逻辑的应用开发框架。2. 核心架构解析ToolJet 是如何工作的要高效地使用一个工具理解其背后的设计思路至关重要。ToolJet 的架构清晰地分为前端构建器Builder和后端执行引擎Executor两部分这种分离设计是其灵活性和可扩展性的基石。2.1 可视化构建器所见即所得的界面组装ToolJet 的构建器是一个运行在浏览器中的富交互应用。它提供了超过60种响应式UI组件从基础的按钮、输入框、容器到复杂的表格、图表折线图、柱状图、饼图、富文本编辑器、地图等。这些组件并非静态的“图片”而是真正的、可交互的 React 组件。组件属性与事件驱动模型这是 ToolJet 交互逻辑的核心。每个组件都有其属性Properties和可触发的事件Events。例如一个“表格”组件其属性包括数据源data、列定义columns、分页设置等。你可以将属性值绑定到一个查询Query的结果或者另一个组件的状态上。而事件比如“行点击”、“按钮点击”则可以触发执行一个或多个“查询”或“控制动作”如显示/隐藏另一个组件。这种基于事件和属性绑定的模型非常类似于现代前端框架如 React、Vue的状态管理思想只不过它被图形化了。你不需要写setState只需要在配置面板里进行拖拽和选择。页面与多页面应用一个 ToolJet 应用可以包含多个页面Page页面之间可以导航。这让你能构建结构相对复杂的应用比如一个后台系统包含“数据概览”、“用户管理”、“系统设置”等多个模块页面。2.2 查询与数据源连接外部世界的桥梁“查询”Query是 ToolJet 中处理数据的核心单元。一个查询代表了一次对外部数据源的操作。ToolJet 支持的数据源种类惊人地丰富主要分为几大类数据库PostgreSQL, MySQL, MongoDB, Elasticsearch, Redis, MS SQL Server, Oracle, Snowflake, BigQuery 等。API 与 SaaS 服务REST API, GraphQL, Stripe, Slack, Google Sheets, SendGrid, Airtable 等。云存储AWS S3, Google Cloud Storage, MinIO。内置 ToolJet 数据库一个开箱即用的、基于 PostgreSQL 的无代码数据库适合存储简单的应用数据。创建查询的过程是可视化的。以连接一个 PostgreSQL 数据库为例你首先在“数据源”配置中填入数据库的连接信息主机、端口、用户名、密码、数据库名。然后在构建器中创建一个“查询”选择这个 PostgreSQL 数据源界面会变成一个 SQL 编辑器。你可以直接编写 SQL也可以使用查询构建器对于简单查询。查询可以接受来自前端组件的输入作为参数例如SELECT * FROM users WHERE department {{ dropdown1.selectedValue }}。查询的链式执行与转换一个查询执行后其返回的数据可以供 UI 组件绑定显示也可以作为另一个查询的输入参数。ToolJet 还允许你在查询返回后使用 JavaScript 对数据进行“转换”Transform比如过滤、排序、映射字段等这为数据处理提供了极大的灵活性。2.3 后端执行与安全模型这是 ToolJet 设计上非常出彩且务实的一点。当你点击一个按钮触发查询时发生了什么前端构建器将查询的定义包括数据源ID、参数、SQL/配置发送到 ToolJet 后端服务器。后端服务器扮演了“安全代理”的角色。它不会将数据库凭证或 API 密钥泄露给前端。相反它用自己的服务身份去执行这个查询。后端连接到目标数据源执行操作获取结果。结果经过可能的转换后返回给前端。这个“代理”模型带来了几个关键好处安全性敏感凭证永远不暴露给客户端浏览器。统一管控可以在后端实施统一的访问控制、速率限制、审计日志。克服 CORS 限制你可以通过 ToolJet 后端去访问那些没有设置 CORS 头的外部 API而无需担心浏览器跨域问题。2.4 部署与扩展性ToolJet 社区版CE是 100% 开源的AGPL v3 协议这意味着你可以完全自主地部署和管理它。它提供了多种部署方式Docker Compose最适合快速启动和单机部署一键拉起所有服务前端、后端、数据库。Kubernetes (Helm Chart)适合生产环境可以方便地水平扩展后端工作节点实现高可用。各大云厂商的 Marketplace 镜像AWS, Azure简化了在云平台上的部署流程。这种可自部署的特性使得企业能够将 ToolJet 深度集成到自己的基础设施中使用内部的私有镜像仓库、配置内部的网络策略和身份认证如 LDAP/OAuth SSO完全掌控数据和应用的命运。3. 实战从零构建一个员工信息管理后台理论说得再多不如亲手做一遍。我们假设一个经典场景为公司 HR 部门构建一个简单的员工信息管理后台。HR 需要能查看员工列表、按部门筛选、查看详情、以及录入新员工。3.1 环境准备与快速启动最快速的体验方式是使用 Docker。确保你的机器上已经安装了 Docker然后执行以下命令docker run \ --name tooljet \ --restart unless-stopped \ -p 80:80 \ --platform linux/amd64 \ -v tooljet_data:/var/lib/postgresql/13/main \ tooljet/try:ee-lts-latest注意这个tooljet/try:ee-lts-latest镜像包含了企业版EE的试用功能。如果你只想使用纯粹的社区版CE可以使用tooljet/tooljet-ce:latest标签。但为了体验完整功能我们这里使用试用镜像。-v参数将 PostgreSQL 数据持久化到名为tooljet_data的 Docker 卷中防止容器重启后数据丢失。命令执行后访问http://localhost你会看到 ToolJet 的初始化页面按照指引创建第一个管理员账户和工作区Workspace。3.2 连接数据源准备你的数据库我们使用一个外部的 PostgreSQL 数据库作为示例。假设你有一个名为company的数据库其中有一张employees表结构如下CREATE TABLE employees ( id SERIAL PRIMARY KEY, employee_id VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, department VARCHAR(50) NOT NULL, position VARCHAR(50), email VARCHAR(100), hire_date DATE, salary DECIMAL(10, 2) );在 ToolJet 工作区内点击左侧导航栏的“数据源”Data Sources然后点击“ 添加数据源”。在列表中找到 “PostgreSQL”点击它。 在弹出的配置窗口中你需要填写名称公司主数据库方便识别的别名主机你的数据库服务器地址如果是本地 Docker 网络内的另一个容器可能是host.docker.internal或服务名端口5432数据库名company用户名/密码你的数据库凭据点击“测试连接”如果显示成功即可保存。这里有一个关键细节在生产环境中强烈建议为 ToolJet 创建一个专用的、权限受限的数据库用户只授予其对特定表如employees的SELECT, INSERT, UPDATE, DELETE权限遵循最小权限原则。3.3 构建应用界面拖拽出雏形创建一个新应用命名为“HR员工管理系统”。进入应用构建器后你会看到一个空白的画布。添加标题和容器从左侧组件库拖拽一个“文本”Text组件到画布顶部修改其text属性为“员工信息管理后台”并调整字体大小和加粗。然后拖拽一个“容器”Container组件作为我们主要内容区的承载面板。创建员工列表表格从组件库拖拽“表格”Table组件到容器内。这是我们的核心组件。默认情况下表格是空的。绑定数据到表格我们需要创建一个查询来获取员工数据。点击画布右上角的“”号选择“查询”。数据源选择我们刚才创建的“公司主数据库”查询类型为“SQL 查询”。在 SQL 编辑器中输入SELECT * FROM employees ORDER BY id DESC;将这个查询命名为getEmployees。点击“运行”按钮下方应该能预览到数据。然后回到表格组件在右侧属性面板中找到“数据”data属性点击绑定按钮{{}}选择getEmployees.data。瞬间表格就充满了数据。美化表格在表格组件的属性中你可以在“列”columns属性中手动调整列的顺序、显示名称如将employee_id显示为“工号”。启用“服务器端分页”如果数据量巨大但我们的示例数据少用客户端分页即可。设置行高、样式等。3.4 实现交互筛选与查看详情现在表格显示了所有员工我们需要让 HR 能按部门筛选。添加筛选下拉框在表格上方拖入一个“下拉选择”Dropdown组件。在它的属性中我们需要手动定义选项Options。假设公司有“技术部”、“市场部”、“人事部”、“财务部”。我们可以直接在options属性中填入一个 JSON 数组[ {label: 全部, value: }, {label: 技术部, value: 技术部}, {label: 市场部, value: 市场部}, {label: 人事部, value: 人事部}, {label: 财务部, value: 财务部} ]设置defaultValue为全部。修改查询使其支持筛选编辑getEmployees查询。我们需要将 SQL 改为动态的接收下拉框的值作为参数。SELECT * FROM employees WHERE CASE WHEN {{ dropdown1.selectedOptionValue }} ! THEN department {{ dropdown1.selectedOptionValue }} ELSE 11 END ORDER BY id DESC;这里用了一个 SQLCASE语句。dropdown1是下拉框组件的默认名称你可以重命名它。{{ ... }}是 ToolJet 的模板语法用于引用前端组件的状态。当选择“全部”值为空字符串时条件11永远为真即查询所有部门。设置查询触发我们希望下拉框的值一变化就自动重新查询。选中下拉框组件在右侧事件Events面板点击“ 添加事件处理程序”事件选择“选项改变时”onOptionChange。在动作Actions中选择“运行查询”然后选择getEmployees。现在切换下拉选项表格数据会实时刷新。实现查看详情模态框我们希望点击表格某一行时弹出一个模态框显示该员工的详细信息。首先拖拽一个“模态框”Modal组件到画布上。默认是隐藏的。将其重命名为detailModal。在模态框内放置多个“文本”组件用于显示员工的各个字段如{{ table1.selectedRow.name }}。然后选中表格组件添加事件处理程序事件为“行点击时”onRowClicked动作为“显示模态框”目标选择detailModal。为了让模态框内容随选中行变化我们需要在显示模态框的同时设置表格的选中行。这可以通过在表格的onRowClicked事件中添加一个“设置组件属性”的动作来实现将table1.selectedRow设置为{{ event.row }}。但通常onRowClicked事件本身就会自动更新table1.selectedRow属性所以模态框内的文本组件直接绑定这个属性即可。3.5 实现增删改表单与数据操作仅有查看功能不够我们需要新增和编辑功能。创建新增员工表单再添加一个按钮文字为“新增员工”点击后显示另一个用于新增的模态框addModal。在addModal中使用“表单”Form组件里面放置多个“输入框”Input、“下拉框”等对应员工的各个字段。创建一个新的查询createEmployee类型为“SQL 查询”使用INSERT语句。SQL 中的值全部绑定表单组件的值例如INSERT INTO employees (employee_id, name, department, position, email, hire_date, salary) VALUES ( {{ components.form1.data.employee_id }}, {{ components.form1.data.name }}, {{ components.form1.data.department }}, {{ components.form1.data.position }}, {{ components.form1.data.email }}, {{ components.form1.data.hire_date }}, {{ components.form1.data.salary }} );重要提示这里存在 SQL 注入的风险因为我们在拼接字符串。对于生产环境ToolJet 的 PostgreSQL 插件支持“预编译查询”Prepared Statement模式。你应该启用该模式并使用?作为占位符然后将组件值作为参数数组传递。这是更安全的做法。在表单的提交按钮事件中依次执行两个动作先运行createEmployee查询成功后运行getEmployees刷新列表并关闭addModal。实现编辑与删除在员工详情模态框detailModal中可以增加“编辑”和“删除”按钮。“编辑”可以跳转到一个新的编辑页面或者复用addModal的形式但将表单初始值设置为{{ table1.selectedRow }}并执行UPDATE查询。“删除”按钮触发一个确认对话框确认后执行DELETE FROM employees WHERE id {{ table1.selectedRow.id }}查询成功后刷新列表。通过以上步骤一个具备增删改查CRUD、筛选、查看详情等基本功能的内部管理后台就搭建完成了。整个过程几乎没有编写传统的“前后端”代码全部通过配置和可视化连接完成。4. 进阶技巧与避坑指南从能用走向好用在实际生产中使用 ToolJet 构建了多个工具后我积累了一些宝贵的经验和踩坑教训。这些是官方文档不一定强调但对稳定性和开发效率至关重要的点。4.1 查询性能与优化避免在查询中处理大量数据ToolJet 查询返回的数据会传输到前端。如果一个查询返回了数万行数据会导致浏览器卡顿甚至崩溃。务必在 SQL 层面做好分页和过滤。充分利用数据库的LIMIT,OFFSET和WHERE子句。善用查询的“缓存”和“防抖”选项对于不常变化的基础数据查询如部门下拉框的选项列表可以设置较长的缓存时间如300秒避免重复请求。对于由输入框触发的搜索查询务必设置“防抖”Debounce延迟如500毫秒防止用户每输入一个字符就发起一次请求。谨慎使用“转换”Transform处理大数据JavaScript 转换是在浏览器端执行的。如果需要对大量数据进行复杂的过滤、排序、映射尽量在数据库查询阶段完成。浏览器的 JavaScript 引擎处理大数据集远不如数据库高效。4.2 状态管理与组件间通信ToolJet 没有像 Redux 或 Vuex 那样显式的全局状态管理但它通过组件的属性和查询结果提供了灵活的状态共享机制。使用“变量”Variables作为全局状态在应用设置中可以定义全局变量。这对于存储用户偏好、应用配置或需要在多个页面间共享的简单状态非常有用。例如可以定义一个currentTheme变量来控制应用的主题色。利用 URL 参数进行页面状态传递在多页面应用中从一个页面跳转到另一个页面时可以通过 URL 参数Query Parameters传递信息。在目标页面可以通过{{ queries.router.query.paramName }}来获取参数值并据此初始化查询。组件命名规范随着应用变复杂画布上会有大量组件。养成给每个组件起一个清晰、有意义的名称的习惯如employeeTable,departmentFilterDropdown,detailModal而不是使用默认的table1,dropdown2。这在编写事件处理和绑定表达式时能极大提高可读性和可维护性。4.3 安全最佳实践数据源权限隔离为不同的应用或用户组创建不同的数据库用户和数据源连接。ToolJet 企业版支持行级、列级的细粒度权限控制社区版则需要通过精心设计的数据源和查询来实现逻辑隔离。禁用不必要的数据源如果某个应用只需要读取权限在创建数据源时可以创建一个只有SELECT权限的数据库用户。对于 REST API 数据源尽量使用具有最小必要权限的 API Token。审计与日志定期检查 ToolJet 服务器日志如果自托管关注异常查询和访问模式。企业版的审计日志功能更完善。网络隔离在生产部署中确保 ToolJet 后端服务器与你的内部数据库、API 服务处于同一个受信任的网络环境中如同一个 VPC避免通过公网传输敏感数据。4.4 部署与运维考量选择 LTS长期支持版本正如官方文档所强调的对于生产环境务必使用 ToolJet 的 LTS 版本而不是最新的latest标签。LTS 版本专注于稳定性、安全补丁和性能修复更适合关键业务。持久化与备份ToolJet 的核心数据用户、应用定义、连接配置默认存储在 PostgreSQL 中。务必确保你的 PostgreSQL 数据卷Volume被正确持久化并建立定期的备份策略。丢失这个数据库意味着丢失所有创建的应用。资源规划ToolJet 后端特别是执行查询的 Worker的内存和 CPU 消耗与并发查询的数量和复杂度正相关。在 Kubernetes 部署中需要为 Pod 设置合理的资源请求Requests和限制Limits并配置 Horizontal Pod Autoscaler 以应对流量高峰。自定义域名与 HTTPS通过 Nginx 或 Traefik 等反向代理为 ToolJet 配置自定义域名和 SSL 证书。这不仅更专业也是启用浏览器某些高级特性如 Service Worker所必需的。4.5 扩展性当内置功能不够用时ToolJet 的强大之处在于它的可扩展性。当预制组件或数据源无法满足需求时你有几条路可以走自定义 JavaScript/Python 代码几乎在任何可以写表达式的地方如组件属性、查询转换你都可以插入 JavaScript 代码段。对于更复杂的后端逻辑可以创建“运行 JavaScript 代码”或“运行 Python 代码”类型的查询。这相当于一个无服务器的函数执行环境你可以在这里进行复杂的数据处理、调用外部库通过导入等。创建自定义组件如果你需要一个特殊的图表或 UI 交互可以使用 ToolJet CLI 开发自己的 React 组件并导入到你的 ToolJet 实例中。这需要前端开发知识但为你打开了无限可能。创建自定义数据源插件如果你需要连接一个 ToolJet 尚未支持的内部或第三方服务可以按照官方指南开发一个数据源插件。这本质上是一个 Node.js 模块定义了如何连接和操作该服务。通过 REST API 数据源连接一切这是最通用的逃生通道。任何能通过 HTTP 访问的服务都可以通过 REST API 数据源接入。你可以用它来连接自建的微服务、古老的 SOAP 接口通过一个适配层或者任何公开的 API。5. 常见问题与故障排查实录在实际部署和使用过程中你肯定会遇到各种问题。下面是我遇到的一些典型问题及其解决方法希望能帮你节省大量搜索时间。5.1 部署与连接问题问题Docker 部署后访问应用非常慢或经常出现“查询执行超时”错误。排查思路资源不足检查 Docker 容器的资源限制。ToolJet 应用本身和其内置的 PostgreSQL 都需要一定内存。尝试增加容器的内存限制如从 2GB 增加到 4GB。数据库性能如果连接的是外部数据库检查该数据库的性能。一个慢查询会拖累整个 ToolJet 的响应。在 ToolJet 的查询编辑器中尝试直接运行简单 SQL如SELECT 1看是否依然慢。网络延迟如果 ToolJet 后端与数据库不在同一网络区域如跨可用区网络延迟会导致查询变慢。尽量将它们部署在相近的位置。解决方案对于自托管建议将 ToolJet 后端与业务数据库部署在同一 VPC 内。监控容器和数据库的资源使用率。问题连接外部数据库如公司内网的 MySQL失败提示“连接被拒绝”或“超时”。排查思路网络连通性从 ToolJet 后端容器内部使用telnet或nc命令测试是否能连接到数据库的主机和端口。如果不行说明网络不通。防火墙与安全组检查数据库服务器的防火墙规则和安全组如果是在云上确保允许来自 ToolJet 后端服务器 IP 地址的入站连接。数据库用户权限确认你使用的数据库用户是否具有从 ToolJet 服务器 IP 地址连接的权限。在 MySQL 中可能需要执行GRANT ... TO usertooljet-server-ip。Docker 网络模式如果 ToolJet 和数据库都运行在 Docker 中确保它们在同一个 Docker 网络Network里并使用容器名或服务名进行连接而不是localhost。解决方案理清网络拓扑。对于生产环境通常将所有相关服务ToolJet 数据库放在一个独立的 Docker 自定义网络或 Kubernetes 集群内。5.2 应用构建与逻辑问题问题下拉框Dropdown的选项Options绑定了一个查询返回的数组但下拉框显示为空白或[object Object]。原因ToolJet 下拉框组件的options属性要求一个特定格式的数组[{“label”: “显示文本”, “value”: “实际值”}, …]。如果你的查询返回的数据是[{“department_name”: “技术部”, “dept_id”: 1}, …]直接绑定是不行的。解决方案使用查询的“转换”Transform功能。在查询编辑器的“转换”选项卡中编写 JavaScript 代码将数据转换为标准格式。例如return data.map(item { return { label: item.department_name, value: item.dept_id }; });然后将转换后的结果绑定到下拉框的options属性。问题表格Table中的数据更新了但页面上的其他组件如汇总数字的文本组件没有随之更新。原因ToolJet 的响应式更新依赖于明确的依赖关系。如果文本组件绑定的表达式是{{ queries.getEmployees.data.length }}那么只有当getEmployees查询重新执行时这个表达式才会重新计算。如果你是通过 JavaScript 代码直接修改了table1.data这个数组不会触发依赖它的组件更新。解决方案要更新数据最佳实践是通过查询来操作数据源然后重新运行获取数据的查询如getEmployees。让数据流保持“查询 - 组件状态”这个单向循环。如果必须在客户端操作可以考虑使用“变量”Variable来存储状态修改变量会触发依赖它的组件更新。问题应用逻辑变得复杂事件处理链条很长难以理解和调试。应对策略模块化设计将大型应用拆分成多个页面每个页面功能相对独立。使用 URL 参数在页面间传递必要状态。善用“注释”Comment组件在画布上放置注释组件用文字描述某一块复杂逻辑的意图就像在代码中写注释一样。命名规范再次强调清晰的组件名、查询名、变量名是维护复杂应用的基石。逐步构建与测试不要一次性构建完所有功能再测试。每添加一个查询和其关联的交互就立刻测试是否工作正常。5.3 权限与多用户协作问题在社区版中如何实现简单的权限控制比如只有管理员能看到“删除”按钮。解决方案社区版没有内置的基于角色的 UI 权限控制。但可以通过变通方法实现利用 ToolJet 的用户系统你可以知道当前登录用户是谁。创建一个查询getCurrentUserRole可以从一个配置文件数据源或简单的 API 返回用户角色。将“删除”按钮的“显示/隐藏”属性visible绑定到一个表达式例如{{ queries.getCurrentUserRole.data.role ‘admin’ }}。在应用加载时或用户登录后执行getCurrentUserRole查询。这是一种基于前端状态的权限控制并非绝对安全关键的数据删除操作必须在后端查询中再次进行权限校验。问题多人同时编辑一个应用时如何避免冲突ToolJet 的机制ToolJet 支持“多玩家编辑”Multiplayer Editing类似于 Google Docs。当多人同时编辑时你可以实时看到其他人的光标位置和编辑动作。应用定义是自动保存的。注意事项虽然实时协作很棒但对于重要的生产应用建议建立变更流程。可以指定应用的“负责人”其他人通过创建副本Fork或分支来进行修改然后由负责人审核合并。企业版的 GitSync 功能为此提供了更工程化的支持。经过几个项目的实战我的体会是ToolJet 最适合的场景是那些业务逻辑明确、交互模式相对标准、但对交付速度要求极高的内部工具。它极大地压缩了从需求到可运行原型的时间。对于开发者而言它把我们从重复的“脚手架”工作中解放出来让我们能更专注于业务逻辑本身。当然它并非银弹对于需要极端定制化 UI、复杂实时交互或超高并发的场景传统的全代码开发仍是更优选择。但在其擅长的领域ToolJet 无疑是一把锋利且趁手的瑞士军刀。最后一个小技巧在构建复杂查询时不妨先在专业的数据库客户端如 DBeaver、pgAdmin里把 SQL 调试完美再复制到 ToolJet 中这能省去很多在 ToolJet 界面内调试 SQL 语法错误的时间。