c# - Entity Framework - 如何将我的特定代码转换为 MySQL 可以理解的表达式

标签 c# mysql regex entity-framework linq

我有一个带有 Entity Framework 和 MySQL 数据库的 ASP.NET MVC 应用程序。我想强制我的代码在 MySQL 服务器上执行所有逻辑。显然我发现了很多有类似问题的案例,但我无法弄清楚我的具体情况。这就是为什么我向你们寻求帮助。

我有一个这样的方法,它对用户输入的空格分隔的单词执行搜索,每个单词应该长于两个字符:

protected List<Book> GetBooks(Search search)
{
    var db = new ProjectDbContext();
    var books = db.Books;

    var listTerms = search.SearchTerm.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries)
        .Where(s => s.Length >= 3).ToList().ConvertAll(t => t.ToLower());

    var searchedBooks = books//.SqlQuery("", );
        .AsEnumerable().Where(book => CheckWhatToSearch(book, search, listTerms));

    var sortedBooks = searchedBooks.OrderBy(search.SortBy + " " + search.SortOrder.ToLower()); // dynamic LINQ query helper

    var pagedBooks = search.HowMuchSkip >= 0 ? 
        sortedBooks.Skip(search.HowMuchSkip).Take(search.HowMuchTake) :
        Enumerable.Empty<Book>().AsQueryable();

    return pagedBooks.ToList(); 
}

当然,运行此方法时,我收到错误,因为 EF 无法将我的自定义函数转换为 SQL 代码

Error: LINQ to Entities does not recognize the method 'Boolean CheckWhatToSearch(MVCDemo.Models.Book, MVCDemo.Models.Search, System.Collections.Generic.List`1[System.String])' method, and this method cannot be translated into a store expression.

CheckWhatToSearch 方法定义如下:

private static bool CheckWhatToSearch(Book book, Search search, List<string> listTerms)
{
    var db = new ProjectDbContext();
    var users = db.Users;

    if (book.IsPublic != true)
        return false; // skip all not public books

    if (listTerms.Count <= 0)
        return true; // if user typed nothing, display entire list of books

    var sbWhereToSearch = new StringBuilder();
    var titleValue = book.Title;
    var authorValue = users.Single(u => u.Id == book.AuthorId).UserName;
    var categoryValue = book.Category;
    var descriptionValue = book.Description;

    if (search.IncludeTitle)
        sbWhereToSearch.Append(titleValue + " ");

    if (search.IncludeAuthor)
        sbWhereToSearch.Append(authorValue + " ");

    if (search.IncludeCategory)
        sbWhereToSearch.Append(categoryValue + " ");

    if (search.IncludeDescription)
        sbWhereToSearch.Append(descriptionValue + " ");

    if (sbWhereToSearch.Length == 0) // default if nothing has been chosen
        sbWhereToSearch.Append(titleValue + " ");

    return listTerms.All(sbWhereToSearch.ToString().ToLower().Contains); // true if all property values concatenated contain all the words typed by user
}

我到底需要弄清楚什么?

  1. 如何重写 CheckWhatToSearch 方法中的代码,以便我可以删除 AsEnumerable() 并强制在 MySQL 服务器上执行所有逻辑。或
  2. What SqlQuery 可以替代我的 CheckWhatToSearch 方法的功能(在本例中我可以直接调用它)

在第二种情况下,我是这样开始的:

DROP PROCEDURE IF EXISTS sp_SearchBooks;
CREATE PROCEDURE sp_SearchBooks(
  IN p_SearchTerms VARCHAR(1000), 
  IN p_IncludeTitle TINYINT, 
  IN p_IncludeAuthor TINYINT, 
  IN p_IncludeCategory TINYINT, 
  IN p_IncludeDescription TINYINT)
BEGIN
  DECLARE v_fieldsToSearch INT DEFAULT "";

  SELECT * FROM tblBooks b
    WHERE 
      LOWER(CONCAT(
        CASE p_IncludeTitle WHEN 1 THEN b.Title ELSE "" END,
        CASE p_IncludeAuthor WHEN 1 THEN (SELECT u.UserName FROM tblUsers u WHERE u.ID = b.AuthorId) ELSE "" END,
        CASE p_IncludeCategory WHEN 1 THEN b.Category ELSE "" END,
        CASE p_IncludeDescription WHEN 1 THEN b.Description ELSE "" END))
      REGEXP REPLACE(p_SearchTerms, " ", "|");
END;

CALL sp_SearchBooks("word1 word2", 1, 1, 0, 0);

但我不喜欢我的方法,我猜它容易受到 SQL 注入(inject)的攻击。除此之外,它匹配 Any,而不是 All(c# 正则表达式与 MySQL 不同,没有 (?=...))。 (SQL程序没写完,我贴出来是为了给大家展示一下我的思路,不过今天我真的很纠结MySQL)

我更喜欢选项 1,仅使用 LINQ。

编辑(20-12-2015 @ 3:30):

好吧,我创建了这样的存储过程:

DROP PROCEDURE IF EXISTS sp_SearchBooks;
CREATE PROCEDURE sp_SearchBooks(
  IN p_SearchTerms VARCHAR(1000), 
  IN p_IncludeTitle TINYINT, 
  IN p_IncludeAuthor TINYINT, 
  IN p_IncludeCategory TINYINT, 
  IN p_IncludeDescription TINYINT)
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE v_currTerm VARCHAR(100) DEFAULT "";

  DROP TEMPORARY TABLE IF EXISTS temp_tblSearchMatches;
    CREATE TEMPORARY TABLE temp_tblSearchMatches
  (
    Id VARCHAR(36),
    SearchTerm VARCHAR(100),
    CONSTRAINT ck_temp_searchmatches_id CHECK (Id REGEXP '[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')
  );

  WHILE (SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(p_SearchTerms, " end;"), ' ', i), ' ', -1) != "end;") DO
    SET v_currTerm = LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(p_SearchTerms, " end;"), ' ', i), ' ', -1));
    INSERT INTO temp_tblSearchMatches (temp_tblSearchMatches.Id, temp_tblSearchMatches.SearchTerm) 
      SELECT b.Id, v_currTerm FROM tblBooks b
        WHERE 
          LOWER(CONCAT(
            CASE p_IncludeTitle WHEN 1 THEN b.Title ELSE "" END, " ",
            CASE p_IncludeAuthor WHEN 1 THEN (SELECT u.UserName FROM tblUsers u WHERE u.ID = b.AuthorId) ELSE "" END, " ",
            CASE p_IncludeCategory WHEN 1 THEN b.Category ELSE "" END, " ",
            CASE p_IncludeDescription WHEN 1 THEN b.Description ELSE "" END)) LIKE CONCAT("%", v_currTerm, "%");
    SET i = i + 1;
  END WHILE;
  COMMIT;
  SELECT b.Id, b.Title, b.Category, b.Description, b.AuthorId, b.Thumbnail, b.AdditionDate, b.IsPublic FROM tblBooks b
    WHERE b.Id IN (
      SELECT sm.Id
        FROM temp_tblSearchMatches sm
        GROUP BY sm.Id
        HAVING COUNT(sm.SearchTerm) = i - 1);
  DROP TEMPORARY TABLE IF EXISTS temp_tblSearchMatches;
END;

修改方法GetBooks

protected List<Book> GetBooks(Search search)
{
    var db = new ProjectDbContext();
    var books = db.Books;

    //var listTerms = search.SearchTerm.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries)
    //    .Where(s => s.Length >= 3).ToList().ConvertAll(t => t.ToLower().Replace("|", ""));

    var paramSearchTerms = new MySqlParameter { ParameterName = "p_SearchTerms", Value = search.SearchTerm };
    var paramIncludeTitle = new MySqlParameter { ParameterName = "p_IncludeTitle", Value = search.IncludeTitle };
    var paramIncludeAuthor = new MySqlParameter { ParameterName = "p_IncludeAuthor", Value = search.IncludeAuthor };
    var paramIncludeCategory = new MySqlParameter { ParameterName = "p_IncludeCategory", Value = search.IncludeCategory };
    var paramIncludeDescription = new MySqlParameter { ParameterName = "p_IncludeDescription", Value = search.IncludeDescription };

    var searchedBooks = books
        .SqlQuery("CALL sp_SearchBooks(@p_SearchTerms, @p_IncludeTitle, @p_IncludeAuthor, @p_IncludeCategory, @p_IncludeDescription)", paramSearchTerms, paramIncludeTitle, paramIncludeAuthor, paramIncludeCategory, paramIncludeDescription);
        //.AsEnumerable().Where(book => CheckWhatToSearch(book, search, listTerms));

    var sortedBooks = searchedBooks.OrderBy(search.SortBy + " " + search.SortOrder.ToLower()); // dynamic LINQ query helper

    var pagedBooks = search.HowMuchSkip >= 0 ? 
        sortedBooks.Skip(search.HowMuchSkip).Take(search.HowMuchTake) :
        Enumerable.Empty<Book>().AsQueryable();

    return pagedBooks.ToList(); 
}

但是现在,我随机收到错误:

“Context cannot be used while the model is being created”

在方法的最后一行实现期间。或死锁(它永远不会到达下一行)。而且我不完全确定我是否能够准确反射(reflect) CheckWhatToSearch 方法功能。

编辑(2015 年 12 月 24 日)

这是我使用存储过程时mysql服务器的操作:

151224  0:38:17    44 Init DB   project
           44 Query CALL sp_SearchBooks('wła', 1, 1, 0, 0)
           44 Init DB   project
           44 Query SELECT
`Extent1`.`Id`, 
`Extent1`.`UserName`, 
`Extent1`.`Password`, 
`Extent1`.`Email`, 
`Extent1`.`RegistrationDate`, 
`Extent1`.`RetryAttempts`, 
`Extent1`.`IsLocked`, 
`Extent1`.`LockedDateTime`
FROM `tblUsers` AS `Extent1`

为什么,以及它到底在哪里调用 select 来检索整个用户表 - 我不知道。而且我仍然陷入僵局。

按照您的建议,我尝试使用动态表达式来实现它,但事实证明它相当困难,你们能帮我吗?

我已经从下面的代码开始,但我被困住了,我不知道如何使用表达式正确编写串联。我想我错过了这一点,因为我不确定在镜像我的方法期间何时应该使用普通变量和方法,以及应该在哪里使用表达式(我猜,列表术语和搜索可以保留原样,并且只能保留那些与书籍相关的内容应该重写):

// Parameter of the main predicate
ParameterExpression pe = Expression.Parameter(typeof(Book), "book");
LabelTarget returnTarget = Expression.Label(typeof(bool));

// if (book.IsPublic != true)
//     return false;
Expression ifBookNotPublic = Expression.IfThen(
    Expression.NotEqual(
        Expression.Property(pe, typeof(Book).GetProperty("IsPublic")),
        Expression.Constant(true)),
    Expression.Return(returnTarget, Expression.Constant(false)));

// if (listTerms.Count <= 0)
//     return true;
Expression paramListTerms = Expression.Constant(listTerms);
Expression ifListTermsCountLessOrEqualThanZero = Expression.IfThen(
    Expression.LessThanOrEqual(
        Expression.Property(paramListTerms, typeof(List<string>).GetProperty("Count")),
        Expression.Constant(0, typeof(int))),
    Expression.Return(returnTarget, Expression.Constant(true)));

// listTerms.All(s => sbWhereToSearch.ToString().ToLower().Contains(s));
ParameterExpression pTerm = Expression.Parameter(typeof(string), "s");
Expression paramSearch = Expression.Constant(search);

// if (search.IncludeTitle)
//     sbWhereToSearch.Append(titleValue + " ");
Expression ifSearchIncludeTitleThenConcat = Expression.IfThen(
    Expression.Equal(
        Expression.Property(paramSearch, typeof(Search).GetProperty("IncludeTitle")),
        Expression.Constant(true)),
    Expression.WHAT NOW ? );


// ===================================
var exprBlock = Expression.Block(); // Expression Calls here
var searchedBooks = books.AsQueryable().Where(Expression.Lambda<Func<Book, bool>>(exprBlock, pe)); // book such as whole block returns true for it

我也尝试了另一种方法,我用匿名函数替换了谓词,它实际上有效,但由于某种未知的原因,mysql日志显示我正在检索两个表,尽管Visual Studio将我的数据显示为可查询并实现仅在最后一行。

最佳答案

通常,您希望将该函数编写为存储过程,就像您开始做的那样。 EF 应该能够识别它,并允许您将其用作上下文中的方法。

但是,您最好将 GetBooks 方法编写为存储过程,然后在请求传入时调用该方法并返回过程结果。这样整个事情就会在数据库引擎上执行,而不是在 Web 服务器上。

这样做的缺点是您将有很多参数传递给存储过程,这可能会使其有点困惑。

关于c# - Entity Framework - 如何将我的特定代码转换为 MySQL 可以理解的表达式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34376388/

相关文章:

c# - 在测试设置和拆卸中重定向 Console.Out

c# - 调试作为 VS 解决方案的一部分运行的 NuGet

php js mysql下拉选择用db值填充文本框

mysql - 哪个查询被优化

MySQL - 替换查询中列中的空格

regex - Vim 正则表达式 - 量词

c# - 为 HTML5 从设备中的线路创建实时音频流

c# - 异步编程并在循环中等待

php - 在 PHP 中打印出以逗号分隔的关联数组列的最简单方法(来自 MYSQL)

java - 根据空格在 java 中拆分一个字符串,用双引号和单引号转义那些空格,前面是\