Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

Sử dụng biểu thức để lọc dữ liệu của cơ sở dữ liệu

Tôi muốn bắt đầu với một mô tả về vấn đề mà tôi gặp phải. Có các thực thể trong cơ sở dữ liệu cần được hiển thị dưới dạng bảng trên giao diện người dùng. Khung thực thể được sử dụng để truy cập cơ sở dữ liệu. Có các bộ lọc cho các cột trong bảng này.

Cần phải viết mã để lọc các thực thể theo tham số.

Ví dụ:có hai thực thể:Người dùng và Sản phẩm.

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Giả sử chúng ta cần lọc người dùng và sản phẩm theo tên. Chúng tôi tạo các phương pháp để lọc từng thực thể.

public IQueryable<User> FilterUsersByName(IQueryable<User> users, string text)
{
    return users.Where(user => user.Name.Contains(text));
}

public IQueryable<Product> FilterProductsByName(IQueryable<Product> products, string text)
{
    return products.Where(product => product.Name.Contains(text));
}

Như bạn có thể thấy, hai phương pháp này gần như giống hệt nhau và chỉ khác nhau về thuộc tính thực thể, dùng để lọc dữ liệu.

Có thể là một thách thức nếu chúng ta có hàng chục thực thể với hàng chục trường yêu cầu lọc. Sự phức tạp nằm ở việc hỗ trợ mã, sao chép thiếu suy nghĩ và kết quả là sự phát triển chậm và xác suất lỗi cao.

Diễn giải Fowler, nó bắt đầu có mùi. Tôi muốn viết một cái gì đó tiêu chuẩn thay vì sao chép mã. Ví dụ:

public IQueryable<User> FilterUsersByName(IQueryable<User> users, string text)
{
    return FilterContainsText(users, user => user.Name, text);
}

public IQueryable<Product> FilterProductsByName(IQueryable<Product> products, string text)
{
    return FilterContainsText(products, propduct => propduct.Name, text);
}

public IQueryable<TEntity> FilterContainsText<TEntity>(IQueryable<TEntity> entities,
 Func<TEntity, string> getProperty, string text)
{
    return entities.Where(entity => getProperty(entity).Contains(text));
}

Thật không may, nếu chúng tôi thử lọc:

public void TestFilter()
{
    using (var context = new Context())
    {
            var filteredProducts = FilterProductsByName(context.Products, "name").ToArray();
    }
}

Chúng tôi sẽ gặp lỗi «Phương pháp kiểm tra ExpressionTests.ExpressionTest.TestFilter đã ném ngoại lệ:
System.NotSupportedException :Loại nút biểu thức LINQ ‘Gọi” không được hỗ trợ trong LINQ tới Thực thể.

Biểu thức

Hãy kiểm tra xem có gì sai không.

Phương thức Where chấp nhận một tham số của kiểu Expression >. Do đó, Linq làm việc với cây biểu thức, nhờ đó nó xây dựng các truy vấn SQL, thay vì với các đại biểu.

Biểu thức mô tả một cây cú pháp. Để hiểu rõ hơn về cách chúng được cấu trúc, hãy xem xét biểu thức, biểu thức này sẽ kiểm tra xem tên có bằng một hàng không.

Expression<Func<Product, bool>> expected = product => product.Name == "target";

Khi gỡ lỗi, chúng ta có thể thấy cấu trúc của biểu thức này (các thuộc tính chính được đánh dấu màu đỏ).

Chúng tôi có cây sau:

Khi chúng ta truyền một tham số dưới dạng ủy nhiệm, một cây khác sẽ được tạo ra, cây này gọi phương thức Gọi trên tham số (ủy nhiệm) thay vì gọi thuộc tính thực thể.

Khi Linq đang cố gắng tạo một truy vấn SQL bằng cây này, nó không biết cách diễn giải phương thức Invoke và ném NotSupportedException.

Do đó, nhiệm vụ của chúng ta là thay thế ép kiểu thành thuộc tính thực thể (phần cây được đánh dấu màu đỏ) bằng biểu thức được truyền qua tham số này.

Hãy thử:

Expression<Func<Product, string>> propertyGetter = product => product.Name;
Expression<Func<Product, bool>> filter = product => propertyGetter(product) == "target"

Bây giờ, chúng ta có thể thấy lỗi «Tên phương thức được mong đợi» ở giai đoạn biên dịch.

Vấn đề là một biểu thức là một lớp đại diện cho các nút của cây cú pháp, chứ không phải là đại biểu và nó không thể được gọi trực tiếp. Bây giờ, nhiệm vụ chính là tìm cách tạo một biểu thức chuyển một tham số khác cho nó.

Khách truy cập

Sau một tìm kiếm ngắn trên Google, tôi đã tìm thấy giải pháp cho vấn đề tương tự tại StackOverflow.

Để làm việc với các biểu thức, có lớp ExpressionVisitor, sử dụng mẫu Khách truy cập. Nó được thiết kế để duyệt qua tất cả các nút của cây biểu thức theo thứ tự phân tích cú pháp và cho phép sửa đổi chúng hoặc trả về một nút khác thay thế. Nếu cả nút và nút con của nó đều không thay đổi, thì biểu thức ban đầu sẽ được trả về.

Khi kế thừa từ lớp ExpressionVisitor, chúng ta có thể thay thế bất kỳ nút cây nào bằng biểu thức mà chúng ta truyền qua tham số. Do đó, chúng ta cần đặt một số nút-nhãn, mà chúng ta sẽ thay thế bằng một tham số, vào cây. Để thực hiện việc này, hãy viết một phương thức mở rộng sẽ mô phỏng lệnh gọi của biểu thức và sẽ là một điểm đánh dấu.

public static class ExpressionExtension
{
    public static TFunc Call<TFunc>(this Expression<TFunc> expression)
    {
        throw new InvalidOperationException("This method should never be called. It is a marker for replacing.");
    }
}

Bây giờ, chúng ta có thể thay thế một biểu thức này bằng một biểu thức khác

Expression<Func<Product, string>> propertyGetter = product => product.Name;
Expression<Func<Product, bool>> filter = product => propertyGetter.Call()(product) == "target";

Cần phải viết một khách truy cập, sẽ thay thế phương thức Gọi bằng tham số của nó trong cây biểu thức:

public class SubstituteExpressionCallVisitor : ExpressionVisitor
{
    private readonly MethodInfo _markerDesctiprion;

    public SubstituteExpressionCallVisitor()
    {
        _markerDesctiprion =
            typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition();
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (IsMarker(node))
        {
            return Visit(ExtractExpression(node));
        }
        return base.VisitMethodCall(node);
    }

    private LambdaExpression ExtractExpression(MethodCallExpression node)
    {
        var target = node.Arguments[0];
        return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke();
    }

    private bool IsMarker(MethodCallExpression node)
    {
        return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == _markerDesctiprion;
    }
}

Chúng tôi có thể thay thế điểm đánh dấu của mình:

public static Expression<TFunc> SubstituteMarker<TFunc>(this Expression<TFunc> expression)
{
    var visitor = new SubstituteExpressionCallVisitor();
    return (Expression<TFunc>)visitor.Visit(expression);
}

Expression<Func<Product, string>> propertyGetter = product => product.Name;
Expression<Func<Product, bool>> filter = product => propertyGetter.Call()(product).Contains("123");
Expression<Func<Product, bool>> finalFilter = filter.SubstituteMarker();

Khi gỡ lỗi, chúng ta có thể thấy rằng biểu thức không như chúng ta mong đợi. Bộ lọc vẫn chứa phương thức Gọi.

Thực tế là các biểu thức Tham sốGetter và finalFilter sử dụng hai đối số khác nhau. Do đó, chúng ta cần thay thế một đối số trong tham sốGetter bằng một đối số trong finalFilter. Để làm điều này, chúng tôi tạo một khách truy cập khác:

Kết quả như sau:

public class SubstituteParameterVisitor : ExpressionVisitor
{
    private readonly LambdaExpression _expressionToVisit;
    private readonly Dictionary<ParameterExpression, Expression> _substitutionByParameter;

    public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit)
    {
        _expressionToVisit = expressionToVisit;
        _substitutionByParameter = expressionToVisit
                .Parameters
                .Select((parameter, index) => new {Parameter = parameter, Index = index})
                .ToDictionary(pair => pair.Parameter, pair => parameterSubstitutions[pair.Index]);
    }

    public Expression Replace()
    {
        return Visit(_expressionToVisit.Body);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        Expression substitution;
        if (_substitutionByParameter.TryGetValue(node, out substitution))
        {
            return Visit(substitution);
        }
        return base.VisitParameter(node);
    }
}

public class SubstituteExpressionCallVisitor : ExpressionVisitor
{
    private readonly MethodInfo _markerDesctiprion;

    public SubstituteExpressionCallVisitor()
    {
        _markerDesctiprion = typeof(ExpressionExtensions)
            .GetMethod(nameof(ExpressionExtensions.Call))
            .GetGenericMethodDefinition();
    }

    protected override Expression VisitInvocation(InvocationExpression node)
    {
        var isMarkerCall = node.Expression.NodeType == ExpressionType.Call &&
                           IsMarker((MethodCallExpression) node.Expression);
        if (isMarkerCall)
        {
            var parameterReplacer = new SubstituteParameterVisitor(node.Arguments.ToArray(),
                Unwrap((MethodCallExpression) node.Expression));
            var target = parameterReplacer.Replace();
            return Visit(target);
        }
        return base.VisitInvocation(node);
    }

    private LambdaExpression Unwrap(MethodCallExpression node)
    {
        var target = node.Arguments[0];
        return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke();
    }

    private bool IsMarker(MethodCallExpression node)
    {
        return node.Method.IsGenericMethod &&
               node.Method.GetGenericMethodDefinition() == _markerDesctiprion;
    }
}

Bây giờ, mọi thứ hoạt động như bình thường và cuối cùng chúng tôi có thể viết phương pháp lọc của mình

public IQueryable<TEntity> FilterContainsText<TEntity>(IQueryable<TEntity> entities, Expression<Func<TEntity, string>> getProperty, string text)
{
    Expression<Func<TEntity, bool>> filter = entity => getProperty.Call()(entity).Contains(text);
    return entities.Where(filter.SubstituteMarker());
}

Kết luận

Cách tiếp cận với sự thay thế biểu thức có thể được sử dụng không chỉ để lọc mà còn để sắp xếp và bất kỳ truy vấn nào đối với cơ sở dữ liệu.

Ngoài ra, phương pháp này cho phép lưu trữ các biểu thức cùng với logic nghiệp vụ riêng biệt với các truy vấn tới cơ sở dữ liệu.

Bạn có thể xem mã tại GitHub.

Bài viết này dựa trên câu trả lời của StackOverflow.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Những gì các bộ lọc ảo làm và không làm, cho bạn biết về độ trễ I / O

  2. Python, Ruby và Golang:So sánh ứng dụng dịch vụ web

  3. Hợp nhất các tệp dữ liệu với Statistica, Phần 2

  4. Di chuyển Dự án Django của bạn sang Heroku

  5. Hiệu suất và bình thường hóa chế độ hàng loạt