简介:面向Delphi 12.3开发者的TMS XData v5.21.0.0控件包,是一套基于REST架构的数据访问框架,用于快速构建可提供增删改查接口的服务端应用,解决网络服务开发中架构选型与组件集成的常见痛点。压缩包为7z格式,体积仅3.2MB,共582个文件;包含249个pas源代码、33个dpr工程文件和29个dpk包文件用于编译安装,76个dfm窗体布局文件,37个html说明页面及chm编译帮助,另有css/js等前端资源与少量pdf/txt说明。整体并非仅仅是库文件,而是完整的组件工程与文档集合。目前已有67人学习下载,对于正在选型或准备上手TMS XData的Delphi开发者具有直接参考价值。通过249个pas源码可深入理解组件内部实现,借助dpr/dpk示例工程和chm帮助手册能快速建立RESTful服务,支持JSON/XML数据交换;包内html/css/js等Web资源可用于前端联调,配合dfm窗体布局文件可降低界面与接口的对接成本。无论初学者还是资深Delphi程序员,都能减少底层网络通信的重复劳动,更专注于业务逻辑,显著缩短数据服务开发周期。
1. TMS XData v5.21.0.0:在 Delphi 12.3 里让业务模型直接长出 REST API 的控件
做 Delphi 后端的人应该都碰过这种局面:Aurelius 实体已经写好了,数据库表也定完了,但前端要调一个GET /customers还得单独搭一套 HTTP 服务,写路由、写 JSON 序列化、写鉴权,折腾半天。TMS XData v5.21.0.0 就是专门解决这个重复劳动的控件集,它基于 Delphi 12.3 编译链,直接把 Aurelius ORM 实体暴露成标准 REST/JSON 资源,自动生成 CRUD 端点,顺带把 JWT 鉴权、CORS、日志这些服务端基础设施也放进组件里。适合已经用 Delphi 做业务系统、需要把数据开放给 Web/H5/移动端复用的团队,也适合想在不换技术栈的前提下做轻量中台的场景。
2. 环境与安装:先让 Delphi 12.3 认识 TMS XData v5.21.0.0
2.1 版本匹配:为什么是 v5.21.0.0 而不是手写 HTTP 服务
先说结论:TMS XData v5.21.0.0 这份资源是按 Delphi 12.3 Athens 编译的,项目的源码路径、BPL 包名、设计期注册逻辑都对齐了这一代 IDE。如果你还在用 Delphi 10.3 或 11,直接装大概率会报DCU版本不匹配或者找不到System.Net的某个类型,这不是资源有问题,而是编译工具链不匹配,后面避坑部分会详细说。
那为什么不手写一个 WebBroker 服务?我早年也这么干过,在TWebModule里自己解析Request.PathInfo、手写Response.Content,再处理 URL 编码和日期格式。短期内接口少还能扛,一旦实体超过十个,每个实体都要写 CRUD 路由、参数校验、分页、排序,代码量翻着倍走,而且所有接口的 JSON 格式靠人肉保证,前端稍微挑点格式问题就得来回返工。XData 在这个场景下更像一个"资源映射器":实体类就是 API 资源,属性就是字段,路由、序列化、HTTP 方法映射全部自动完成。
2.2 安装步骤:解压、加库路径、编译安装
拿到的是 .7z 压缩包,不是 installer,所以安装要做几步手工操作,但每一步都可控。先把压缩包释放到一个不带空格的目录,我的习惯是放D:\Libs\tms\xdata_521,不建议丢到系统盘或者带中文名的路径,Delphi 的库路径对中文路径支持一直不太稳定。
接下来打开 Delphi 12.3,做三件事:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | Tools > Options > Environment Variables里新增变量 | 变量名TMS_XDATA,指向解压根目录,后续库路径可以直接引用 |
| 2 | Tools > Options > Delphi Options > Library > Library Path追加源码路径 | 把$(TMS_XDATA)\source(或对应版本目录)加进去,让 IDE 能索引到单元文件 |
| 3 | 打开包工程组并顺序编译 | 在packages目录下找到 Delphi 12 对应的.groupproj,先编译运行时包(.dpk),再编译设计期包并右键Install |
注意第三步的顺序很关键,XData 的运行时包依赖 Aurelius 的运行时包,如果直接把设计期包拿去 Install,会因为找不到底层 DCU 报错。正确顺序是从下往上编译:先Aurelius,再XData核心,最后才是设计期注册包。看到Design package installed的提示并且 Tool Palette 里出现TMS XData分类,就说明装上了。
2.3 验证安装:用自带 Demo 建立第一个服务器
XData 安装包通常会带示例工程,路径一般在demos或samples目录下。优先找名字带WebModule或者Server的示例,打开后编译运行,浏览器访问http://localhost:8080/tms/xdata/info,如果返回一段 JSON,列出 XData 版本号和服务状态,就说明整条链路是通的。
这里有个关键的验证点:/tms/xdata是 XData 的默认根路径,后面跟的info是框架自带的服务信息接口。如果浏览器访问能看到类似"product" : "TMS XData"的字段,说明不仅是组件装好了,连接池和请求分发也正常。如果这一步就 404,优先查端口是不是被别的服务占了,再查 WebModule 有没有被正确加载。
3. 模型先行:用 Aurelius 实体把数据表变成 REST 资源
3.1 数据模型即 API:XData 如何做映射
XData 的核心设计理念是"模型即接口",它不做独立的接口编排层,而是直接读取 Aurelius 实体的元数据。实体类上的[Entity]、属性上的[Column]这些注解,在运行期被 XData 翻译成资源的名称、字段的 JSON 键名和类型。也就是说,你写实体类的时候,接口文档其实已经同步确定了。
这意味着两件事:第一,字段名、类型、是否可空都会直接影响 API 表现,设计实体类时要站在前端视角想"这个字段能不能为 null、应该返回字符串还是整数";第二,XData 端点的资源名默认是实体类名的复数形式,比如TCustomer对应的资源路径就是/customers,如果你在代码里写的是TUserInfo,那路径就是/userInfo,大小写和复数化规则要心里有数,否则前端按文档调不到。
3.2 写一个 TCustomer 实体:注解、主键与默认命名
下面是最小可运行的实体定义,以客户表为例:
unit uCustomer; interface uses Aurelius.Mapping.Attributes; type [Entity] [Table('Customers')] TCustomer = class private [Column('Id', [TColumnProp.Identity])] FId: Integer; [Column('Name', [])] FName: string; [Column('Email', [])] FEmail: string; public property Id: Integer read FId write FId; property Name: string read FName write FName; property Email: string read FEmail write FEmail; end; implementation end.这段代码里有两个容易忽略的参数:TColumnProp.Identity表示主键自增,XData 在生成POST /customers接口时会认为不需要客户端传入 Id,而PUT /customers(1)则会要求 URL 里的(1)与实体主键匹配。如果主键属性没标Identity,创建资源的接口就要求你在 JSON Body 里显式传主键,行为完全不同。
3.3 多表和关联关系:Master-Detail 资源怎么暴露
实际业务很少只有单表,XData 对关联关系的处理是:实体属性直接引用另一个实体类,JSON 序列化时会输出嵌套对象。举个例子,订单和客户的关系:
type [Entity] [Table('Orders')] TOrder = class private [Column('Id', [TColumnProp.Identity])] FId: Integer; [Column('CustomerId', [])] FCustomerId: Integer; [Column('TotalAmount', [])] FTotalAmount: Double; FCustomer: TCustomer; public property Id: Integer read FId write FId; property CustomerId: Integer read FCustomerId write FCustomerId; property TotalAmount: Double read FTotalAmount write FTotalAmount; [Association([TAssociationProp.LazyLoading])] property Customer: TCustomer read FCustomer write FCustomer; end;注意Association注解上的LazyLoading参数。我一般会默认打开懒加载,这样查询订单列表时不会把每个订单的客户对象都带出来,JSON 体积会小很多。前端需要客户详情时,再单独调/customers(2)取数据,这也是 REST 接口更合理的使用方式。如果不加懒加载,订单量大时响应会明显变慢,这是新手最容易踩的坑。
4. 把 CRUD 暴露成 API:XData 端点、筛选参数和自定义业务方法
4.1 自动 CRUD 端点:GET/POST/PUT/DELETE 映射规则
实体类编译进去之后,XData 会自动注册一套标准端点,映射关系非常直观:
| HTTP 方法 | URL 示例 | 行为 |
|---|---|---|
| GET | /customers | 返回客户列表,默认分页 100 条 |
| GET | /customers(15) | 返回单条记录,主键为 15 |
| POST | /customers | 创建记录,Body 放 JSON 对象 |
| PUT | /customers(15) | 整体更新记录 15 |
| DELETE | /customers(15) | 删除记录 15 |
这套映射不需要写一行路由代码。需要留意的坑是 URL 里主键的括号写法是 OData 风格,不是常见的/customers/15,前端如果按路径参数的习惯拼接就会 404。我一般会在接口文档里显式告诉前端这个规则,顺手在 WebModule 的响应头里加上X-XData-EntitySet: customers之类的自定义头,方便排查问题。
4.2 分页、筛选与排序:$top/$skip/$filter 的实际用法
XData 对列表接口默认支持一组查询前缀参数,这是它区别于"一个实体一个固定接口"的另一个优点。看一个实际请求:
GET /tms/xdata/customers?$top=20&$skip=40&$filter=TotalAmount gt 1000&$orderby=Name desc参数含义拆开说:
$top=20只取前 20 条,等价于 SQL 里的LIMIT 20。$skip=40跳过前 40 条,和$top配合做分页。前端常见的"第 3 页每页 20 条"就是$skip=40&$top=20。$filter=TotalAmount gt 1000是服务端过滤,gt是大于的意思,XData 会把它翻译成 SQL 的 WHERE 条件,不会出现"查出全量再在内存里过滤"这种灾难。$orderby=Name desc指定排序字段和方向,多个字段用逗号分隔。
这套参数是可以叠加的,但要注意$filter的字段名必须和 JSON 序列化后的字段名一致。如果你在实体属性上用了[Column('full_name')],但属性名是FName,那 URL 里应该写full_name还是Name?以 JSON 键名为准,这个细节在联调前先确认一次,能省半天排查时间。
4.3 自定义业务方法:在服务类上开新的路由
自动 CRUD 覆盖的是标准增删改查,但业务里总有"查询某用户最近十笔订单"这种聚合逻辑。XData 允许在服务器端注册服务类,把方法暴露成额外端点。我常用的做法是在独立的单元里定义服务接口,不写进实体类:
type TCustomerService = class public function GetRecentOrders(CustomerId: Integer; Count: Integer = 10): TList<TOrder>; end;服务器启动时把它注册进 XData 的服务集合里,前端就可以直接请求:
GET /tms/xdata/Service/GetRecentOrders?CustomerId=5&Count=10这个路径的规则是Service固定,后面跟着类名和方法名,参数走查询字符串。好处是业务逻辑留在 Delphi 端,可以继续用 Aurelius 的查询语法、事务和连接池,不用为了一个聚合查询去写裸 SQL。我建议自定义服务方法统一走这种模式,而不是另开一个 HTTP 端口。
4.4 客户端调用:用原生 RESTClient 做最小联调
虽然 TMS 提供了官方的 XData Client 组件,但我联调时更习惯先用 Delphi 自带的TNetHTTPClient把接口调通,少一层依赖,出了问题也好判断是服务端还是客户端的事:
uses System.Net.HttpClient, System.JSON; var HttpClient: TNetHTTPClient; Response: IHTTPResponse; JSONValue: TJSONValue; begin HttpClient := TNetHTTPClient.Create(nil); try Response := HttpClient.Get('http://localhost:8080/tms/xdata/customers?$top=10'); if Response.StatusCode = 200 then begin JSONValue := TJSONObject.ParseJSONValue(Response.ContentAsString(TEncoding.UTF8)); try // JSONValue 里就是客户列表数组 WriteLn(JSONValue.ToString); finally JSONValue.Free; end; end; finally HttpClient.Free; end; end;TNetHTTPClient.Get返回的IHTTPResponse里包含状态码、响应头和 Body,状态码 200 是正常,401 是鉴权失败,404 多半是资源名和实体名没对上。这里用TJSONObject.ParseJSONValue解析响应,不引入额外的 JSON 库,联调阶段够用了。
5. 避坑与排查:XData 部署中常见的七个翻车现场
5.1 安装后 IDE 里找不到 XData 组件面板
现象:按步骤编译了设计期包,Install 也提示成功,但 Tool Palette 里搜不到TMS XData。原因有两个,一是库路径里源码目录顺序不对,IDE 索引到了旧版本的 DCU;二是安装了很多第三方组件,组件包分类被折叠了。解决:在 Tool Palette 右上角搜索框里直接输入TXDataServer,如果搜不到,回到Components > Install Packages里确认设计期包名字前打了勾,再把库路径里旧版路径删掉,重启 IDE。
5.2 GET 请求返回 500,服务端日志却一片空白
现象:浏览器访问/customers返回 500 Internal Server Error,WebModule 的 OnLog 事件什么都没打。原因通常是实体类的属性映射问题,比如数据库表里没有Name列,但实体里写了Name,这个错误在 XData 生成查询语句时才抛出,而异常被框架吞进了默认日志通道。解决:先在浏览器访问/tms/xdata/info确认服务正常,再直接调用/customers(1)缩小范围,最后检查实体类映射和数据库字段是否一一对应。
5.3 JSON 日期时间格式和前端预期不一致
现象:前端拿到的是2024-05-12T08:30:00.000Z,但页面显示时多了 8 小时。原因:Delphi 的TDateTime本身没有时区概念,XData 序列化时按服务器本地时间输出,而前端 JavaScript 的new Date()默认按浏览器时区渲染。解决:在服务器端统一定义时间基准,我一般会在部署文档里约定所有接口返回 UTC 时间,前端拿到后再转当地时间。如果是内部系统不想改逻辑,就在前端解析时忽略时区字段,按纯字符串处理。
5.4 SQLite 并发请求时频繁报"database is locked"
现象:请求量稍微上来,SQLite 为后端的服务就开始间歇性 500。原因:SQLite 默认单连接串行写,XData 的连接池在并发写场景下会同时打开多个连接,其中一个拿到锁,其他就排队失败。解决:SQLite 适合接口量小、并发低的内部工具;如果要做多用户在线服务,把数据库切到 MySQL/SQL Server,连接字符串改一下,应用代码不用动。这个项目里如果已经踩了 SQLite 的坑,别犹豫,趁早换库。
5.5 CORS 配置不生效,前端跨域被浏览器拦截
现象:前端跑在localhost:4200,后端跑在8080,浏览器控制台报跨域错误,但代码里明明设置了Access-Control-Allow-Origin。原因:CORS 的响应头必须要在预检请求阶段返回,也就是OPTIONS请求也要有对应处理,很多人只处理了 GET/POST 而漏掉了 OPTIONS。解决:在 WebModule 的 BeforeDispatch 事件里统一加三个响应头:
Response.SetCustomHeader('Access-Control-Allow-Origin', '*'); Response.SetCustomHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); Response.SetCustomHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-XData-*');Access-Control-Allow-Headers里的Authorization是给 JWT 用的,Content-Type是给 JSON 请求体用的。注意*在需要携带 Cookie 的场景下不适用,那时要写具体域名。
5.6 URL 资源名大小写导致 404
现象:实体类叫TUserInfo,前端访问/userinfo是通的,但访问/userInfo就 404。原因:XData 的资源名映射保留了实体类名的原始大小写,而且默认复数化时不改大小写。解决:约定所有资源名统一小写访问,或者在实体类上用[Entity('userinfo')]显式指定资源名,不要让前端去猜大小写规则。
5.7 部署到 Windows 服务后模块不加载
现象:开发环境一切正常,放到服务器上注册成 Windows 服务,一启动就报"模块不存在"或"找不到指定的模块"。原因:跑 WebBroker 应用需要依赖Delphi 12.3的运行时 DLL(如rtl290.bpl等),服务器上没装对应版本运行时库。解决:发布时选用Release Static编译模式,并勾选Link with runtime packages取消勾选,让 BPL 依赖打进 EXE。如果你是做绿色发布,记得把XData和Aurelius注册运行库包也做好静默安装脚本,不然换台机器就翻车。
6. 收尾技巧:给 XData 服务加日志、鉴权和联调脚本
6.1 用拦截器记录每次请求耗时
生产环境里最怕的就是"接口偶尔慢一下",但说不出具体是哪个请求慢。XData 服务端的WebModule里有两个事件可以利用:BeforeDispatch和AfterDispatch。我在BeforeDispatch里记录开始时间,在AfterDispatch里计算耗时,再拼上请求路径、查询参数、状态码,一行结构化日志就出来了:
procedure TXDataWebModule.WebModuleBeforeDispatch(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); begin FStartTick := TStopwatch.GetTimestamp; end; procedure TXDataWebModule.WebModuleAfterDispatch(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var ElapsedMs: Int64; begin ElapsedMs := (TStopwatch.GetTimestamp - FStartTick) * TStopwatch.TicksPerMillisecond; LogRequest(Request.PathInfo, Request.Query, Response.StatusCode, ElapsedMs); end;这一段代码简单直接,只要在LogRequest里把信息写进日志文件或数据库,就足够定位百分之八十的性能问题。慢查询的特征很统一:$filter条件没走索引、$skip页数太大、关联对象懒加载被逐条触发,日志里一看耗时就能确定方向。
6.2 配合 FireMonkey 客户端快速联调
XData 做服务端,Delphi 12.3 的 FireMonkey 做客户端,是同一个技术栈下很舒服的组合。我一般会在客户端工程里放一个调试窗体,放一个TEdit填 URL,一个TMemo看返回结果,再放几个按钮对应 GET、POST、PUT、DELETE。联调时不需要断点,直接在这个调试面板里发请求,返回的 JSON 格式化打印出来,前端同学要什么字段一眼就能对照。
这个方法每次调试都帮我筛掉一类问题:到底是接口数据不对,还是客户端解析逻辑不对。接口返回的 JSON 在调试面板里先看一遍,再让前端去对接,沟通成本低很多。
从那以后我每次接手 XData 项目,都会强制自己先把/tms/xdata/info、实体映射、调试面板这三件事走一遍,确认环境是健康的再去碰业务逻辑。希望这篇笔记能帮你少踩几个坑,把 XData 真正用起来。
本文还有配套的精品资源,点击获取