簡體   English   中英

將 OData $filter 解析為 SQL Where 子句

[英]Parsing OData $filter into SQL Where clause

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

有沒有人已經寫過類似的東西,我可以用它作為起點? 或者就一個好的、可靠的方法來實現這一目標提供建議?

您需要對原始值應用一些 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%'

雖然沒有直接幫助 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被故意禁用, EnableConstantParameterization聯過濾器值,而不必為每個過濾器參數跟蹤和注入 SqlParameter。 它簡化了代碼,並且已經在一定程度上進行了消毒。 如果這不能滿足您的安全問題,則取決於您是否付出額外的努力。
  • 您會注意到我過濾到查詢中的LAST WHERE子句,這是因為如果此查詢涉及投影並且調用者嘗試將過濾器應用於輔助范圍之一(連接的結果集),那么 EF 將通過過濾子查詢,而不是在最后應用所有過濾器。 有辦法解決這個問題或使用它,現在讓我們堅持一個簡單的例子。

modifiedQuery生成的 SQL:

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

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 腳本,你應該能夠運行它,但是你需要。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM