【问题标题】:Parsing OData $filter into SQL Where clause将 OData $filter 解析为 SQL Where 子句
【发布时间】:2017-02-10 07:40:27
【问题描述】:

我需要从使用 ODATA 的 Web API 服务器 (C#) 查询旧数据库中的表。我有一个用于旧数据库的基本 ODBC 驱动程序,此时我只需要支持基本过滤(eq、startswith 和 substringof)。例如:

queryOptions.Filter.RawValue:

( (startswith(Name,'Bill'))  and  
(substringof('sunset',Address))  and  
(substringof('7421',Phone)) )

应该转换成这样的(我这里只关心WHERE子句):

SELECT CustName, Address1, Address2, ... 
FROM Customers
WHERE CustName like 'Bill%' AND 
  Address1 like '%sunset% AND 
  Phone like '%7421%'

我意识到解析 RawValue 可能不是一个好主意。

有没有人已经写了类似的东西可以作为起点?或者建议一个好的、可靠的方法来完成这个?

【问题讨论】:

标签: c# sql asp.net-web-api odata


【解决方案1】:

您需要对原始值应用一些正则表达式,获取匹配项并应用一些逻辑来进行转换。 基本上,搜索带有参数的函数,删除函数文本,获取参数并将它们转换为类似的子句。 像这样的:

string str = @"( (startswith(Name,'Bill'))  and  
(substringof('sunset',Address))  and  
(substringof('7421',Phone)) )";

System.Text.RegularExpressions.Regex regex = new  System.Text.RegularExpressions.Regex(@"startswith\(([^\)]+)\)");

System.Text.RegularExpressions.Match match = regex.Match(str);

if (match.Success)
{
  string tmp = match.Value;
  string destination = "@field LIKE '@val%'";

  tmp = tmp.Replace( "startswith(","");
  tmp = tmp.Replace( ")","");

  string[] keyvalue = tmp.Split(',');
  string field = keyvalue[0];
  string val = keyvalue[1];

  destination = destination.Replace("@field", field);
  destination = destination.Replace("@val", val.Replace("'",""));
  Console.WriteLine( destination );
}

这个输出:

Name LIKE 'Bill%'

【讨论】:

  • @epsino316 谢谢,但是使用 reg ex 解析原始值可能有点脆弱(ODATA 查询过滤器基于用户在 UI 中输入的任何内容,包括会破坏 regex 的字符,除非逃脱等)。我希望找到一种更强大的方法(无需编写自己的查询提供程序)。
  • 没有它什么都想不出来。可能是一些商业转换工具,或者在 github 中寻找一个开源的特定项目。
【解决方案2】:

虽然没有直接帮助 OP,但多年来我一直回到这个问题,并开发了另一个技巧,如果您当前的架构接近旧数据库,您可以使用。

这仅适用于您可以针对 EF 上下文创建类似或相同的查询,我们将利用 Linq to Entity SQL 表别名约定,因此它可能会受到未来更新的影响。

  1. 定义一个与您的输出表结构非常接近的 EF 查询。
  2. 使用FilterQueryOption.ApplyTo()$filter 应用于近似查询
  3. 从查询中捕获 SQL 字符串
  4. 从查询中提取WHERE 子句
  5. WHERE 子句注入到您的自定义查询中。

除了与 EF 注入的表别名约束相关联之外,与单独使用 REGEX 相比,这提供了很多安全性和灵活性。您可能会发现您可以使用正则表达式来进一步增强此输出,但是 OData 解析器已经将 URL 表达式验证并清理为有效的 SQL 语法,包括将表达式转换为 SQL 函数调用。

以下基于 EF6 和 OData v4,因此 URL 语法略有不同,但相同的概念也应适用于 ODataLib 的早期版本。

  • CustomDTO 是自定义类,未在 EF DbContext 模型中定义。
  • Customer IS 在 EF DbContext 中定义,它具有与旧数据库相似的字段
/// <summary>Return a list of customer summaries for a given Account</summary>
[EnableQuery, HttpGet]
public IQueryable<CustomDTO> Customers([FromODataUri] int key, ODataQueryOptions<CustomDTO> _queryOptions)
{
    // The custom query we want to apply to the legacy database.
    // NOTE: If the fields are identical to the current DbContext, then we don't have to go this far.
    // We MUST alias the table to match the generated SQL
    string sql = "SELECT CustName, IsNull(Address1,'') + IsNull(Address2,'') as Address, Phone " + 
                 "FROM Customers AS [Extent1]" + 
                 "WHERE AccountId = @AccountId";
    if (!String.IsNullOrWhiteSpace(_queryOptions.Filter?.RawValue))
    {
        var criteriaQuery = from x in db.Customers
                            select new CustomDTO
                            {
                                Name = CustName,
                                Address = Address1 + Address2
                                Phone = Phone
                            };
        var modifiedQuery = _queryOptions.Filter.ApplyTo(criteriaQuery, new ODataQuerySettings({ EnableConstantParameterization = false });
        string modifiedSql = modifiedQuery.ToString();
        modifiedSql = modifiedSql.Substring(modifiedSql.LastIndexOf("WHERE ") + 5);
        sql += $" AND ({modifiedSql})";
    }

    var customers = aDifferentContext.Database.SqlQuery<CustomDTO>(sql, new SqlParameter("@AccountId", key)).ToList();
    return customers.AsQueryable();
}
  • 在我们的自定义查询中使用别名 [Extent1] 的替代方法是使用字符串替换,但这已经足够了。
  • EnableConstantParameterization 被故意禁用,以内联过滤器值,而不必为每个过滤器参数跟踪和注入 SqlParameter。它简化了代码,并且已经在一定程度上进行了清理。如果这不能满足您的安全问题,您需要付出额外的努力。
  • 您会注意到我过滤到查询中的 LAST WHERE 子句,这是因为如果此查询涉及投影并且调用者尝试将过滤器应用于辅助范围之一(加入结果集)然后 EF 将通过过滤子查询来优化查询,而不是在最后应用过滤器。有一些方法可以解决这个问题或使用它,现在让我们举一个简单的例子。

modifiedQuery生成的SQL:

网址: ~/OData/Accounts(1102)/Customers?$filter=startswith(Name, 'Bill') and contains(Address, 'sunset') and contains(Phone, '7421')

Filter.RawValue: startswith(Name, 'Bill') and contains(Address, 'sunset') and contains(Phone, '7421')

SELECT 
    [Extent1].[CustName] AS [Name], 
    CASE WHEN ([Extent1].[Address1] IS NULL) THEN N'' ELSE [Extent1].[Address1] END + CASE WHEN ([Extent1].[Address2] IS NULL) THEN N'' ELSE [Extent1].[Address2] END AS [C1], 
    [Extent1].[Phone] AS [Phone]
    FROM  [dbo].[Customer] AS [Extent1]
    WHERE ([Extent1].[CustName] LIKE 'Bill%') 
      AND (CASE WHEN ([Extent1].[Address1] IS NULL) THEN N'' ELSE [Extent1].[Address1] END 
              + CASE WHEN ([Extent1].[Address2] IS NULL) THEN N'' ELSE [Extent1].[Address2] END 
           LIKE N'%sunset%') 
      AND ([Extent1].[Phone] LIKE '%7421%')

最终执行的 SQL:

SELECT CustName as Name, IsNull(Address1,'') + IsNull(Address2,'') as Address, Phone 
  FROM  [dbo].[Customer] AS [Extent1]
 WHERE AccountId = @AccountId AND (([Extent1].[CustName] LIKE 'Bill%') 
      AND (CASE WHEN ([Extent1].[Address1] IS NULL) THEN N'' ELSE [Extent1].[Address1] END 
              + CASE WHEN ([Extent1].[Address2] IS NULL) THEN N'' ELSE [Extent1].[Address2] END 
           LIKE N'%sunset%') 
      AND ([Extent1].[Phone] LIKE '%7421%'))

类定义

public class CustomDTO
{
    public string Name { get;set; }
    public string Address { get;set; }
    public string Phone { get;set; }
} 

public class Customer
{
    public int AccountId { get;set; }
    public string CustName { get;set; }
    public string Address1 { get;set; }
    public string Address2 { get;set; }
    public string Phone { get;set; }
}

我主要在优化复杂的 Linq 表达式时使用这个技巧,这些表达式返回 DTO 结构,可以使用比 EF ca 生成的更简单的 SQL 来实现。将传统的 EF 查询替换为DbContext.Database.SqlQuery&lt;T&gt;(sql, parameters) 形式的原始 SQL 查询

在此示例中,我使用了不同的 EF DbContext,但是一旦您拥有 SQL 脚本,您应该能够根据需要运行它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-09-13
    • 1970-01-01
    • 1970-01-01
    • 2020-02-09
    • 1970-01-01
    • 2011-08-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多