将OData $ filter解析为SQL Where子句

时间:2016-10-02 02:37:42

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

我需要从使用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可能不是一个好主意。

有没有人写过类似的东西我可以作为起点?或建议以一种良好,可靠的方式来实现这一目标?

2 个答案:

答案 0 :(得分:0)

您需要将一些Regex应用于原始值,获取匹配并应用一些逻辑来进行转换。 基本上,使用参数搜索函数,删除函数文本,获取参数并将它们转换为like子句。 像这样:

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%'

答案 1 :(得分:0)

虽然没有直接帮助 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 表达式时使用这个技巧,这些表达式返回可以用比 EF ca 生成的更简单的 SQL 实现的 DTO 结构。传统的 EF 查询被 DbContext.Database.SqlQuery<T>(sql, parameters)

形式的原始 SQL 查询所取代

在这个例子中,我使用了一个不同的 EF DbContext,但是一旦你有了 SQL 脚本,你应该能够以任何你需要的方式运行它。