简体   繁体   English

将 OData $filter 解析为 SQL Where 子句

[英]Parsing OData $filter into SQL Where clause

I need to query tables in a legacy database from a Web API server (C#) that uses ODATA.我需要从使用 ODATA 的 Web API 服务器 (C#) 查询旧数据库中的表。 I have a basic ODBC driver for the legacy database and I only need to support basic filtering at this time (eq, startswith and substringof).我有一个用于旧数据库的基本 ODBC 驱动程序,此时我只需要支持基本过滤(eq、startswith 和 substringof)。 For example:例如:

queryOptions.Filter.RawValue: queryOptions.Filter.RawValue:

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

Should be converted into something like this (I am only concerned with the WHERE clause here):应该转换成这样的(我只关心这里的 WHERE 子句):

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

I realize that parsing the RawValue is probably not a great idea.我意识到解析 RawValue 可能不是一个好主意。

Does anybody have something similar already written that I can use as a starting point?有没有人已经写过类似的东西,我可以用它作为起点? Or advice on a good, reliable way to accomplish this?或者就一个好的、可靠的方法来实现这一目标提供建议?

You will need to apply some Regex to the raw value, obtain the matches and apply some logic to make the transformation.您需要对原始值应用一些 Regex,获取匹配项并应用一些逻辑来进行转换。 Basically, search the functions with the parameters, remove the function text, get the parameters and transform them into the like clauses.基本上,搜索带有参数的函数,删除函数文本,获取参数并将它们转换为 like 子句。 Something like this:像这样的东西:

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 );
}

This outputs:这输出:

Name LIKE 'Bill%'

Whilst not directly helping OP I keep coming back to this question over the years and have developed another trick you can use if your current schema is close to the legacy database.虽然没有直接帮助 OP,但多年来我一直回到这个问题,并开发了另一个技巧,如果您当前的架构接近遗留数据库,您可以使用它。

This will only apply if you can create a query that is similar or the same against the EF context, we will be exploiting the Linq to Entity SQL table alias conventions and as such it may be affected by future updates.这仅适用于您可以针对 EF 上下文创建相似或相同的查询时,我们将利用 Linq to Entity SQL 表别名约定,因此它可能会受到未来更新的影响。

  1. define an EF query that closely approximates the table structure of your output.定义一个非常接近输出表结构的 EF 查询。
  2. use FilterQueryOption.ApplyTo() to apply just the $filter to the approximated query使用FilterQueryOption.ApplyTo()$filter应用于近似查询
  3. Capture the SQL string from the query从查询中捕获 SQL 字符串
  4. Extract the WHERE clause from the query从查询中提取WHERE子句
  5. Inject the WHERE clause into your custom query.WHERE子句注入您的自定义查询。

Apart from being tied to the table alias constraints the EF injects, this offers a lot of security and flexibility over using REGEX alone.除了绑定到 EF 注入的表别名约束之外,这比单独使用 REGEX 提供了很多安全性和灵活性。 you might find that you can use regex to further enhance this output, however the OData parser will already validate and sanitise the URL expression into valid SQL syntax, including transposing the expression into SQL function calls.您可能会发现可以使用正则表达式进一步增强此输出,但是 OData 解析器已经将 URL 表达式验证并清理为有效的 SQL 语法,包括将表达式转换为 SQL 函数调用。

The following is based on EF6 and OData v4, so the URL syntax is a bit different, but the same concept should apply to previous versions of ODataLib as well.以下基于 EF6 和 OData v4,因此 URL 语法略有不同,但相同的概念也应适用于以前版本的 ODataLib。

  • CustomDTO is a custom class, not defined in the EF DbContext model. CustomDTO是一个自定义类,未在 EF DbContext 模型中定义。
  • Customer IS defined in the EF DbContext, it has similar fields to the legacy database 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();
}
  • An alternative to using the alias [Extent1] in our custom query would be to use string replacement, but this works well enough.在我们的自定义查询中使用别名[Extent1]的另一种方法是使用字符串替换,但这已经足够了。
  • EnableConstantParameterization was deliberately disabled, to inline the filter values, instead of having to track and inject the SqlParameter for each filter argument. EnableConstantParameterization被故意禁用, EnableConstantParameterization联过滤器值,而不必为每个过滤器参数跟踪和注入 SqlParameter。 It similifies the code and is already sanitised to a degree.它简化了代码,并且已经在一定程度上进行了消毒。 Its up to you to put in the extra effort if this does not satisfy your security concerns.如果这不能满足您的安全问题,则取决于您是否付出额外的努力。
  • You will notice I filtered to the LAST WHERE clause in the query, that is because if this query involved projections and the caller tried to apply a filter to one of the secondary extents (joined result sets) then EF will optimise the query by filtering the sub query, rather than applying the filters all at the end.您会注意到我过滤到查询中的LAST WHERE子句,这是因为如果此查询涉及投影并且调用者尝试将过滤器应用于辅助范围之一(连接的结果集),那么 EF 将通过过滤子查询,而不是在最后应用所有过滤器。 There are ways around this or to work with it, for now lets stick to a simple example.有办法解决这个问题或使用它,现在让我们坚持一个简单的例子。

SQL Generated by modifiedQuery :modifiedQuery生成的 SQL:

URL: ~/OData/Accounts(1102)/Customers?$filter=startswith(Name, 'Bill') and contains(Address, 'sunset') and contains(Phone, '7421') URL: ~/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') 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%')

The final SQL that gets executed:执行的最终 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%'))

Class Definitions类定义

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; }
}

I use this trick mostly when optimising complex Linq expressions that return DTO structures that can be implemented with much simpler SQL than EF ca produce.我主要在优化复杂的 Linq 表达式时使用这个技巧,这些表达式返回可以使用比 EF ca 生成的更简单的 SQL 实现的 DTO 结构。 What was a traditional EF query is replaced with a raw SQL query in the form of DbContext.Database.SqlQuery<T>(sql, parameters)什么是传统的 EF 查询被替换为DbContext.Database.SqlQuery<T>(sql, parameters)形式的原始 SQL 查询

In this example I used a different EF DbContext, but once you have the SQL script, you should be able to run that however you need to.在这个例子中,我使用了一个不同的 EF DbContext,但是一旦你有了 SQL 脚本,你应该能够运行它,但是你需要。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM