动态api设计文档
用模板引擎的方式撰写一段Sql脚本模板,并指明对应的参数含义,发布后前端可以通过统一的端点地址直接调用。
设计目标 #
目标: 用模板引擎的方式撰写一段Sql脚本模板,并指明对应的参数含义,发布后前端可以通过 /api/dynamic-api/invoke/my-api-name
的方式直接调用,且调用支持多种HTTP方法和以不同的方式传递参数,如:查询参数、body
和header
参数方式。该设计思路与: http://www.51dbapi.com 类似。
基本过程 #
api定义(ApiDefinition
): 定义一个api,指明唯一的name
和备注信息等,并指明它的sql脚本和数据库链接等。
连接池(ConnectionPool
): 多个api请求数据库的数据时共享同一个连接池,减少建立链接的过程。为api指定连接池,即指明对应的数据库链接地址,则直接执行调用;
api参数(ApiParameter
): 定义一个api需要的参数,该参数的名称需要上述sql脚本使用的脚本保持一致。
配置动态api的过程如下:
- 提供表单,用户填写api的基本信息,如:英文名称、请求方法、备注信息等;
- 用户填写api运行时要执行的sql脚本,注意这里要使用到模板引擎,有很多判空的逻辑。本系统推荐使用
liquid
模板。 - 为该api指明sql脚本执行时调用哪个链接,即使用哪个数据库。弹出连接池列表选中一个。
- 用户填写api是否支持分页和排序,若支持,则自动含有分页和排序对应的查询参数,如:pageSize\MaxResult\Sorting等。
- 用户为该api添加参数,参数分为:查询参数、body参数和header参数,另外也需要添加参数的名称。这些参数需要与第2步中,脚本引擎引用的变量名称保持一致。
- 发布该api,配置该api的可访问
permissions
。 - 测试api的可用性,提供参数的输入界面,点击测试后能返回对应的结果。
liquid
模板的sql脚本实例如下:
{% assign param1 = 'value1' %}
{% assign param2 = 'value2' %}
{% assign param3 = null %}
{% assign sql_query = "SELECT * FROM your_table WHERE 1=1" %}
{% if param1 != null %}
{% assign sql_query = sql_query | append: " AND column1 = '" | append: param1 | append: "'" %}
{% endif %}
{% if param2 != null %}
{% assign sql_query = sql_query | append: " AND column2 = '" | append: param2 | append: "'" %}
{% endif %}
{% if param3 != null %}
{% assign sql_query = sql_query | append: " AND column3 = '" | append: param3 | append: "'" %}
{% endif %}
{{ sql_query }}
可类比C#模板引擎: https://github.com/scriban/scriban , abp使用了该模板引擎,主要看对if语句的兼容情况,以及要保持语言简洁。
Handlebars
模板引擎来构造 SQL 查询语句的示例:
{{#with params}}
SELECT * FROM your_table WHERE 1=1
{{#if param1}} AND column1 = '{{param1}}'{{/if}}
{{#if param2}} AND column2 = '{{param2}}'{{/if}}
{{#if param3}} AND column3 = '{{param3}}'{{/if}}
{{/with}}
razor
语法:
string template = @"
@{
var param1 = Model.param1;
var param2 = Model.param2;
var param3 = Model.param3;
// 构造 SQL 查询语句
var sqlQuery = ""SELECT * FROM your_table WHERE 1=1 order by "";
if (!string.IsNullOrEmpty(param1))
{
sqlQuery += "" AND column1 = '"" + param1 + ""'"";
}
if (!string.IsNullOrEmpty(param2))
{
sqlQuery += "" AND column2 = '"" + param2 + ""'"";
}
if (!string.IsNullOrEmpty(param3))
{
sqlQuery += "" AND column3 = '"" + param3 + ""'"";
}
}
@sqlQuery";
var model = new { param1 = "value1", param2 = (string)null, param3 = "value3" };
string result = Engine.Razor.RunCompile(template, "templateKey", null, model);
动态api的返回对象 #
当IsMultipleResult=true 或 IsPaging = true
时:
{
"totalCount": 100,
"items":[{
// ... 具体的实体
}]
}
当IsMultipleResult=false
时:
{
// 具体的实体结构
}
数据库设计 #
数据库连接池 ConnectionPool
,
字段名 | 逻辑名 | 数据类型 | 长度 | 约束 | 说明 |
---|---|---|---|---|---|
Id | 主键Id | String | 36 | 主键 | Id |
DisplayText | 显示名称 | String | 64 | 连接池名称 | |
ConnectionString | 链接字符串 | String | 256 | 链接字符串 | |
DbType | 数据库类型 | String | 64 | 数据库类型,如: Oracle、MySql等 | |
Host | 主机地址 | String | 100 | 主机地址 | |
Port | 端口号 | String | 10 | 端口号 | |
UserName | 用户名 | String | 32 | 用户名 | |
Password | 密码 | String | 128 | 密码 | |
IsActive | 是否激活 | Bool | 1 | 是否激活,可用状态 | |
PoolSize | 连接池大小 | Integer | 32 | 连接池大小 | |
MaxConcurrency | 最大并发量 | Integer | 32 | 最大并发量 | |
Remark | 备注 | String | 备注 |
API定义 ApiDefinition
字段名 | 逻辑名 | 数据类型 | 长度 | 约束 | 说明 |
---|---|---|---|---|---|
Id | 主键Id | String | 64 | 主键 | Id |
Name | 名称 | String | 64 | 非空 | API名称,英文 |
Category | 类别 | String | 32 | 类别,默认为default | |
ConnectionPoolId | 连接池Id | String | 64 | 非空 | 连接池Id,表明从哪个数据库获得数据 |
HttpMethod | 请求方法 | String | 32 | 非空 | 请求方法,如:get\post\delete\put等 |
ScriptTemplate | 脚本模板 | String | 1024 | 非空 | 脚本模板,如使用一段 liquid代码 |
ScriptType | 脚本类型 | String | 64 | 脚本类型,如: sql 语句, 或存储过程 procedure,默认为sql | |
TemplateEngine | 模板引擎 | String | 32 | 非空 | 如使用liquid解析模板 |
IsMultipleResult | 是否是多行记录 | Bool | 1 | 是否返回多行记录,此设置影响返回结果的格式 | |
IsPaging | 是否分页 | Bool | 1 | 是否支持分页,若支持则自动有 pageSize 和 maxResult查询参数,不需要配置 | |
IsSorting | 是否排序 | Bool | 1 | 是否支持排序,若支持则自动有 sorting查询参数,不需要配置 | |
Permissions | 需要的权限 | String | 512 | 逗号隔开,只有对应的权限才可以访问该api,不填则不需要权限 | |
Rank | 顺序值 | Integer | 32 | 顺序值 | |
Remark | 备注 | String | 备注信息 |
IsMultipleResult: 是用来指示返回结果的格式,为true时结果集为数组,也影响执行sql语句后对结果的处理上。
IsPaging: 指示是否分页,若分页就不需要在ApiParameter表增加pageSize和maxResult的查询参数,另外当IsPaging=true时, 必然 IsMultipleResult = true。
IsSorting: 指示是否排序,若有排序就不需要在ApiParameter表增加sorting的查询参数,在脚本模板里可以引用sorting字段。
API参数 ApiParameter
字段名 | 逻辑名 | 数据类型 | 长度 | 约束 | 说明 |
---|---|---|---|---|---|
Id | 主键Id | String | 64 | 主键 | Id |
Name | 名称 | String | 64 | 非空 | 参数名称 |
DisplayText | 显示的中文文本 | String | 128 | 显示的中文文本 | |
DataType | 数据类型 | String | 32 | 数据类型,如:float,integer,string,bool等。 | |
ParamType | 参数类型 | String | 32 | 非空 | 参数类型,如:查询参数、路径参数(基本不大可能)、header参数或body请求体 |
Multiple | 是否是数组 | Bool | 1 | 非空 | 是否是数组 |
Required | 是否是必需的 | Bool | 1 | 是否是必需的 | |
DefinitionId | Api的定义Id | String | 64 | 该参数属于哪个Api定义的 | |
Rank | 顺序值 | Integer | 32 | 顺序值 |
接口设计 #
public interface ITemplateEngine{
public string Name { get; }
Task<string> Render(string template, IDictionary<string, object> input = null);
}
public class JsTemplateEngine: ITemplateEngine{
}
public class LiquidTemplateEngine: ITemplateEngine{
}
public class TemplateEngineManager {
public void RegisterEngine(string engineName){}
public ITemplateEngine GetEngine(string engineName);
}
public class ConnectionManager{
// 先实现 Oracle和 mysql的 连接池获取
// 需要用到连接池 ConnectionPool
public IDbConnection GetConnection(Guid connectionPoolId){
}
public void ReturnConnection(IDbConnection connection){}
}
public class ExecutionContext{
public ExecutionContext(string apiName, ApiDefinition definition, IEnumerable<ApiParameter> parameters){
ApiName = apiName;
Definition = definition;
Parameters = parameters;
}
public string ApiName{get; protected set;}
public ApiDefinition Definition{get; protected set;}
public IEnumerable<ApiParameter> Parameters{ get; protected set;}
public bool CheckParameters(IDictionary<string, object> input){
return true;
}
}
public interface IDynamicApiExecutor{
Task<object> ExecuteAsync(IDictionary<string, object> input);
}
public class DefaultDynamicApiExecutor: IDynamicApiExecutor{
protected readonly TemplateEngineManager _templateEngineManager;
protected readonly ConnectionManager _connectionManager;
public async Task<object> ExecuteAsync(ExecutionContext context, IDictionary<string, object> input = null){
// 1.检查入参:根据传入的input和Parameters定义,检查 input的类型以及是否必需等信息,若不满足则抛出异常
bool isSuccess = context.CheckParameters(input);
// 2.渲染模板: 根据定义和对应的引擎类型,传入input获得渲染后的sql语句或调用存储过程的语句
var engine = _templateEngineManager.GetEngine(context.Definition.TemplateEngine);
string sqlQuery = engine.Render(context.Definition.ScriptTemplate, input);
// 3.获取链接:根据Definition的ConnectionPoolId从连接池中获得链接
IDbConnection connection = _connectionManager.GetConnection(context.Definition.ConnectionPoolId);
// 4.执行sql: 建立链接,并启动调用。根据定义返回对应的结果
var result = connection.ExecuteSql(sqlQuery);
// 5.归还链接:归还到连接池中
_connectionManager.ReturnConnection(connection);
return result;
}
}
public class DynamicApiManager{
protected IDynamicApiExecutor _executor;
private async Task<ExecutionContext> BuildContext(string apiName){
// var definition = _apiDefinitionRepository.FindAsync(s=>s.Name == apiName);
return new ExecutionContext();
}
public async Task<object> ExecuteAsync(string apiName, IDictionary<string, object> input = null){
var context = await BuildContext(apiName);
return await _executor.ExecuteAsync(context, input);
}
public async Task<bool> CheckPermission(string apiName, string userId){
var permissions = await repository.GetApiPermissions(apiName);
return await _permissionManager.CheckPermissions(permissions, userId);
}
}
Endpoint设计 #
IQueryCollection
解决查询参数不确定的问题。IFormCollection
解决POST方法时表单参数不确定的问题。
using Microsoft.AspNetCore.Http;
protected DynamicApiManager _dynamicApiManager;
[HttpPost("invoke/{apiName}")]
public IActionResult InvokePost(string apiName, IFormCollection formData, IQueryCollection queryParameters)
{
var input = new Dictionary<string, object>(2);
// 处理请求体参数
formData.ForEach(s => input[s.Key] = s.Value);
// 处理查询参数
queryParameters.ForEach(s => input[s.Key] = s.Value);
var result = _dynamicApiManager.ExecuteAsync(apiName, input);
return Ok(result);
}
[HttpGet("invoke/{apiName}")]
public IActionResult InvokeGet(string apiName, IQueryCollection queryParameters)
{
var input = new Dictionary<string, object>(2);
queryParameters.ForEach(s => input[s.Key] = s.Value);
var result = _dynamicApiManager.ExecuteAsync(apiName, input);
return Ok(result);
}
[HttpPut("invoke/{apiName}")]
public IActionResult InvokePut(string apiName, IFormCollection formData, IQueryCollection queryParameters)
{}
[HttpDelete("invoke/{apiName}")]
public IActionResult InvokeDelete(string apiName, IFormCollection formData, IQueryCollection queryParameters)
{}
注意:上述Action的URL地址配置都是一样的,但是HTTP方法都不一样,那么就可以实现调用侧只需要使用不同的方法但是相同的url地址实现api接口的使用。