XData -–无需开发、基于配置的数据库RESTful服务,可作为移动App和ExtJS、WPF/Silverlight、Ajax等应用的服务端
源起一个App项目,Web服务器就一台,已经装了管理系统(ASP.NET Web MVC),因此要求App的服务端也采用微软.NET技术。经历(小试)了raw SQL、EF、最终采用了XData(斯人云:传来传去都是JSON,搞得那么复杂干嘛)。
从Remoting到WCF再到ASP.NET Web API2,从RPC过渡到REST,构建Http服务从复杂到简单,成本也越来越低。微软发起的OData - the Best Way to REST协议,2014年的4.0版本已成为工业标准。搭建一个遵循OData协议的 REST服务微软的标准做法是使用EF + ASP.NET Web API2。服务端架构大致如此:Table/View(Databse) ßEFàObject + ßMicrosoft.AspNet.ODataàJSON/XML(ASP.NET Web API2)。XData去掉了中间的Object层,其服务端架构: Table/View(Databse) ßXFà JSON/XML(ASP.NET Web API2)。显然XData 同样也是 ASP.NET Web API2服务,只是把微软的EF - Entity Framework换成了XF - Element Framework。XData在实现上尽量符合OData协议标准,做了一些适当地修改。下表列出EF与XF的区别:
需要开发人员的EF - Entity Framework |
不需要开发人员,(只)需要配置(维护)人员的XF - Element Framework |
ORM - Object-Database Map |
XML-Database Map |
有状态 |
无状态 |
基于编码,开发需要VS |
基于配置,无需开发环境,只需一个XML或文本编辑器 |
支持SQL Server各个版本,支持多种数据库 |
目前只支持SQL Server 2012及以上,可以扩展支持各种数据库 |
第零篇 启动XData服务
要求:
.NET Framework 4.5
SQL Server 2012(2008勉强可用,但分页会报错)
Google Chrome 浏览器(便于观察服务端的响应,Chrome会在页面上显示XML、IE 则会提示下载JSON文件),下文浏览器如未作说明,均指Google Chrome。
VS2012(当然也可以部署到IIS,在VS2012打开,是为了让你看到代码)
第一步:从http://xdata.codeplex.com/SourceControl/latest下载源代码,解压后,用VS2012 打开XData.sln;
第二步:在VS2012的解决方案资源管理器中(显示所有文件)找到instances文件夹下的default.xf.xml实例配置文件,打开文件找到<connectionString>节点,将其value属性值改成你的数据库连接字符串;
第三步:将浏览器设置为Google Chrome,解决方案配置设置为Debug,按F5启动调试后,VS2012会打开Chrome 浏览器,新建一个标签页,其地址栏为:http://localhost:2012/,页面显示:HTTP 错误 403.14 - Forbidden Web 服务器被配置为不列出此目录的内容。在地址栏输入http://localhost:2012/Schemas
页面显示如下:
<Index>
<NameMapConfig>http://localhost:2012/Schemas/Primary/NameMap</NameMapConfig>
<DatabaseSchema>http://localhost:2012/Schemas/Primary/Database</DatabaseSchema>
<Config>http://localhost:2012/Schemas/Primary/Config</Config>
<Schema>http://localhost:2012/Schemas/Primary/Schema ...
至此,已经成功启动XData服务( Web API)。
第一篇 浏览器里的查询
在浏览器地址栏键入http://localhost:2012/XData/$now返回服务器当前日期时间
键入http://localhost:2012/XData/$utcnow返回服务器当前UTC日期时间,何为UTC,自行Google之。
第二篇 浏览器里的“SQL”之单表查询
假设你的数据库中有一个账户表:User(还假设有如下字段,行记录)。有如下SQL语句:
SELECT Id, UserName FROM User WHERE Id > 1 ORDER BY UserName ASC;
对应的XData查询为:
注1:%20表示空格,你在输入时直接键入空格,浏览器会自动编码为%20。
注2:XData严格区分大小写,所有关键字(保留字)均为小写。
$select、$orderby 与SQL类似。
/ SetOfUser 相当FROM子句,形为”SetOf” + 表名(为什么是”SetOf”+,而不是” ArrayOf”+,或“Users”,这是由实例配置文件配置项指定的,见XF文档)。
WHERE Id > 1 的XData写法为:$filter=Id gt 1。XData以$filter对应WHERE子句,且=、>、<等符号均(2个字母)字符串化。
下面列出常用的几个操作符:
详细内容见XF文档。由于XData 高度模仿(尽量遵循)OData(标准),亦请参考OData。
回车后,浏览器页面显示如下:
<SetOfUser>
<User>
<Id>2</Id>
<UserName>Cherry</UserName>
</User>
<User>
<Id>3</Id>
<UserName>John</UserName>
</User>
…
</SetOfUser>
注:如浏览器为IE,会下载一个SetOfUser.json,内容为:{"SetOfUser":{"User":[{"Id":"2","UserName":"Cherry"},{"Id":"3","UserName":"John"}]}}。
分页查询
假设有如下SQL语句:
SELECT Id, UserName FROM User WHERE Id > 1 ORDER BY UserName ASC;
$top,$skip
一般地,分页还需返回总行记录数,以下XData查询:http://localhost:2012/XData/SetOfUser/$count? $filter=Id%20gt%201
返回:<Count>12</Count>
第三篇 浏览器里的“SQL”之联合查询
假设数据库中还有一个雇员表:Employees,它与账户表User为一对多的关系,即一个雇员可能有多个账户。
以下SQL在数据库中定义了两个表之间的关系(外键):
ALTER TABLE [dbo].[User] WITH CHECK ADD CONSTRAINT [FK_User_Employee] FOREIGN KEY([EmployeeId])
REFERENCES [dbo].[Employee] ([Id])
注:应该在数据库中定义外键等约束吗?作者倾向于否,请自行思考之。
如果你在数据库定义了两个表之间的关系,XData(XF)会自动生成如下:
<Relationship From="User" To="Employee" Type="ManyToOne" Content="User(EmployeeId),Employee(Id)"/>
反之,上面的<Relationship>就要手写在实例配置文件<primaryConfigGetter>节点指定的XML文件中。
有了上面的<Relationship>,可以有如下的XData查询:
http://localhost:2012/XData/SetOfUser?$select=Id,UserName,Employee.Name&$filter=Id%20gt%201
我们把形如Employee.Name的字段称之为外联字段,外联字段可以出现在任何本表字段出现的位置上。
XData还支持多对多查询,如账户表和角色表之间的多对多关系:
http://localhost:2012/XData/SetOfUser?$select=Id,UserName,Role.RoleName&$filter=Id%20gt%201
前提是存在:
<Relationship From="UserRole" To="Role" Type="ManyToOne" Content="UserRole(RoleId),Role(Id)"/>
<Relationship From="UserRole" To="User" Type="ManyToOne" Content="UserRole(UserId),User(Id)"/>
XData(XF)会自行推导出:
<Relationship From="Role" To="User" Type="ManyToMany" Content="Role(Id),UserRole(RoleId);UserRole(UserId),User(Id)"/>
间接的关系(非直接关系)
http://localhost:2012/XData/SetOfUser?$select=Id,UserName,Role.RoleName,Job.Name&$filter=Id%20gt%201
前提是存在:
<Relationship From="User" To="Employee" Type="ManyToOne" Content="User(EmployeeId),Employee(Id)"/>
<Relationship From="Employee" To="Job" Type="ManyToOne" Content="Employee(JobId),Job(Id)"/>
XData(XF)也会自行推导出:
<ReferencePath From="User" To="Job" Content="User(EmployeeId),Employee(Id);Employee(JobId),Job(Id)"/>
<ReferencePath>用来定义单向多对一多级关系。
更远的关系,譬如还有表:JobLevel,并且存在:
<Relationship From="User" To="Employee" Type="ManyToOne" Content="User(EmployeeId),Employee(Id)"/>
<Relationship From="Employee" To="Job" Type="ManyToOne" Content="Employee(JobId),Job(Id)"/>
<Relationship From="Job" To=" JobLevel" Type="ManyToOne" Content="Job(LevelId),JobLevel(Id)"/>
下面的查询会报错:
XData(XF)不会推导比间接更远的关系。
要使上述查询成功,需在实例配置文件<primaryConfigGetter>节点指定的XML文件中手写:
<ReferencePath From="User" To="JobLevel" Content="User(EmployeeId),Employee(Id);Employee(JobId),Job(Id);Job(LevelId),JobLevel(Id)"/>
默认(缺省)值查询
以下查询返回一条填写好默认值的行记录XML:
http://localhost/XData/User/$default
还可以通过指定$select来限定字段和加入外联字段,同样需要相应的<Relationship>和<ReferencePath>存在。
注:XData(XF)不是简单填入默认值,而是会执行一条SQL来获取外联字段值。
重要的FAQ
如果两个表之间存在多个(一个以上)关系,XData(XF),如何抉择?
XData(XF)会报错。因此,需要指定其中一个关系为“主要”的关系,以<Prime>指出:
<Relationship …>
<Prime/>
</Relationship>
追问:如果一个查询同时要用到两个表之间多个关系,又如何?
见下篇(自定义)查询(包)。
思考:以上查询,基表与外联表都是单向多对一(或多对多),如果多对一查询会怎样?有兴趣可以一试,XData(XF)不会报错。
第四篇 浏览器里的“SQL”之聚合(多级主从表)查询
通过$expand可以带出当前行记录包含的子行记录,如以下查询:
http://localhost:2012/XData/SetOfEmployee?$expand=SetOfUser&$filter=Id%20gt%201
返回的Master-Detail:
<SetOfEmployee>
<Employee>
<Id>2</Id>
…
<SetOfUser>
<User>
<Id>2</Id>
…
</User>
…
</SetOfUser>
</Employee>
…
</SetOfEmployee>
还可以在$expand中指定$select、$filter等查询选项,如:
嵌套(多级)$expand,如:
http://localhost:2012/XData/SetOfJob?$expand=SetOfEmployee($expand=SetOfUser)
当然$expand也支持多对多,如:
http://localhost:2012/XData/SetOfUser?$expand=SetOfRole
XData(XF)多对多处理原则:XData(XF)不会处理多对多的下一层级。也就是说,遇到多对多,就此打住,不再更深层次的推进了。
第五篇POST查询 -(自定义)查询(包)
以上查询,都是用HTTP GET方法,提交(请求 request)的数据都写在地址栏里。POST请求(request),需要工具(如Fiddler)或编写代码(或做一个查询页面)来提交数据。下面就以上述分页查询举一个例子:
<Query Method="GetSet">
<User>
<Select>Id,UserName</Select>
<Filter>Id gt 1</Filter>
<OrderBy>UserName asc</OrderBy>
<Top>2</Top>
<Skip>4</Skip>
</User>
</Query>
查询request包以<Query>作为根节点,Method属性指出查询方法,并把$select、$fileter、$orderby、$top、&skip和$expand(上例无此项),写在子节点里。查询方法,即Method属性值有三个,分别是"GetDefault"、"GetCount"、"GetSet"。
高阶查询包:
POST查询相比GET查询高阶之处,在于能自定义Schema。何谓Schema?Schema相当与元数据(数据库结构),但这个元数据是可配置的。XData(XF) 通过不同的Schema,来配置(影响)各个(读/写)方法的结果。
现在可以来回答第三篇 浏览器里的“SQL”之联合查询 结尾的追问:如果一个查询同时要用到两个表之间多个关系,又如何?
譬如有一个员工请假单表LeaveForm,有字段Id、LeaveType(病假、事假、年休等),还有请假人字段EmployeeId和批准经理字段ManagerId。
与Employee表,有:
<Relationship From="LeaveForm" To="Employee" Type="ManyToOne" Content="LeaveForm(EmployeeId),Employee(Id)"/>
<Relationship From="LeaveForm" To="Employee" Type="ManyToOne" Content="LeaveForm(ManagerId),Employee(Id)"/>
查询包如下,黄底加亮的为Config名称,两处必须一致:
<QueryMethod="GetSet"Config="forGetSet">
<ConfigName="forGetSet">
<LeaveFormSet="SetOfLeaveForm">
<Employee.NameElement="Employee"Field="Name"Relationship.Content="LeaveForm(EmployeeId),Employee(Id)"/>
<Manager.NameElement="Employee"Field="Name"Relationship.Content="LeaveForm(ManagerId),Employee(Id)"/>
</LeaveForm>
</Config>
<LeaveForm>
<Select>Id,LeaveType,Employee.Name,Manager.Name</Select>
<Filter>EmployeeId eq 1</Filter>
</LeaveForm>
</Query>
注:Element属性在这里就是表名。有关表名、列名与Element名、Field名之间的关系(映射),参见XF文档。在此文中,表名与Element名一样,列名与Field名一样,暂不作区分。
在Schema中可以为外联字段指定Relationship.Content,当然也可以是ReferencePath. Content。
如果为<Relationship>指定了Name属性,也可以用Relationship.Name替代Relationship.Content,<ReferencePath>也是一样。一般地,会为每个<ReferencePath>指定Name属性,而较少为<Relationship>指定Name属性。
当两表之间关系唯一时,可以省略Relationship.Content或ReferencePath.Content或.Name,XData(XF)会自行推导。其优先顺序按“亲疏”关系排列:带有<Prime>标记的ManyToOne;带有<Prime>标记的ManyToMany;带有<Prime>标记的ReferencePath;不带<Prime>标记的ManyToOne;不带<Prime>标记的ManyToMany;不带<Prime>标记的ReferencePath。
第六篇POST查询 -(自定义)查询(包)之Expand
有如下查询:
<QueryMethod="GetSet"Config="forGetSet">
<ConfigName="forGetSet">
<EmployeeSet="SetOfEmployee">
<Job.NameElement="Job"Field="Name"/>
</Employee>
</Config>
<Employee>
<Select>Id,Name,Job.Name</Select>
<Filter>Id eq 1</Filter>
<Expand>SetOfUser($select=Id,UserName$orderby="UserName")</Expand>
</Employee>
</Query>
与下面查询结果是一样的:
<QueryMethod="GetSet"Config="forGetSet">
<ConfigName="forGetSet">
<EmployeeSet="SetOfEmployee">
<Job.NameElement="Job"Field="Name"Relationship.Content="Employees(JobId),Jobs(Id)"/>
<SetOfUserElement="User"Select="Id,UserName"OrderBy="UserName"Relationship.Name="Employee-User"/>
</Employee>
<RelationshipName="Employee-User"From="Employee"To="User"Type="OneToMany"Content="Employees(Id),Users(EmployeeId)"/>
</Config>
<Employee>
<Select>Id,Name,Job.Name</Select>
<Filter>Id eq 1</Filter>
</Employee>
</Query>
注意两者的黄底加亮部分,实际上在运行时XData(XF)会把前一种转换为后一种再调用更底层的查询。灰底部分在这里其实是多余的,因为XData(XF)会自行推导,只有在遇到第五篇这种同时要用到两个表之间多个关系的情况时方才有用,显然此时只能用后一种查询。