Database
 sql >> डेटाबेस >  >> RDS >> Database

डेटाबेस के डेटा को फ़िल्टर करने के लिए अभिव्यक्तियों का उपयोग करना

मैं उस समस्या के विवरण के साथ शुरू करना चाहता हूं जिसका मुझे सामना करना पड़ा। डेटाबेस में ऐसी इकाइयाँ हैं जिन्हें UI पर तालिकाओं के रूप में प्रदर्शित करने की आवश्यकता है। डेटाबेस तक पहुँचने के लिए एंटिटी फ्रेमवर्क का उपयोग किया जाता है। इन तालिका स्तंभों के लिए फ़िल्टर हैं।

मापदंडों के आधार पर इकाइयों को फ़िल्टर करने के लिए एक कोड लिखना आवश्यक है।

उदाहरण के लिए, दो निकाय हैं:उपयोगकर्ता और उत्पाद।

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

मान लें कि हमें उपयोगकर्ताओं और उत्पादों को नाम से फ़िल्टर करने की आवश्यकता है। हम प्रत्येक इकाई को फ़िल्टर करने के तरीके बनाते हैं।

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

जैसा कि आप देख सकते हैं, ये दो विधियां लगभग समान हैं और केवल निकाय गुण में भिन्न हैं, जिसके द्वारा वह डेटा को फ़िल्टर करती है।

यह एक चुनौती हो सकती है यदि हमारे पास दर्जनों इकाइयाँ हैं जिनमें दर्जनों फ़ील्ड हैं जिन्हें फ़िल्टर करने की आवश्यकता है। जटिलता कोड समर्थन, विचारहीन प्रतिलिपि, और इसके परिणामस्वरूप, धीमी गति से विकास और त्रुटि की उच्च संभावना में है।

पैराफ्रेशिंग फाउलर, यह महकने लगता है। मैं कोड डुप्लिकेशन के बजाय कुछ मानक लिखना चाहता हूं। उदाहरण के लिए:

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

दुर्भाग्य से, अगर हम फ़िल्टर करने का प्रयास करते हैं:

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

हमें त्रुटि मिलेगी «टेस्ट विधि ExpressionTests.ExpressionTest.TestFilter ने अपवाद फेंक दिया:
System.NotSupportedException :LINQ व्यंजक नोड प्रकार 'आमंत्रण' समर्थित नहीं है LINQ से संस्थाओं में।

अभिव्यक्ति

आइए देखें कि क्या गलत हुआ।

जहां विधि अभिव्यक्ति> प्रकार के पैरामीटर को स्वीकार करती है। इस प्रकार, लिंक एक्सप्रेशन ट्री के साथ काम करता है, जिसके द्वारा यह प्रतिनिधियों के बजाय SQL क्वेरी बनाता है।

अभिव्यक्ति एक सिंटैक्स ट्री का वर्णन करती है। यह बेहतर ढंग से समझने के लिए कि उनकी संरचना कैसे की जाती है, उस व्यंजक पर विचार करें, जो यह जांचता है कि कोई नाम एक पंक्ति के बराबर है या नहीं।

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

डिबगिंग करते समय, हम इस अभिव्यक्ति की संरचना देख सकते हैं (प्रमुख गुण लाल रंग में चिह्नित हैं)।

हमारे पास निम्न पेड़ है:

जब हम एक प्रतिनिधि को पैरामीटर के रूप में पास करते हैं, तो एक अलग पेड़ उत्पन्न होता है, जो इकाई संपत्ति को आमंत्रित करने के बजाय (प्रतिनिधि) पैरामीटर पर इनवोक विधि को कॉल करता है।

जब लिंक इस ट्री द्वारा एक SQL क्वेरी बनाने का प्रयास कर रहा है, तो यह नहीं जानता कि Invoke विधि की व्याख्या कैसे करें और NotSupportedException को फेंकता है।

इस प्रकार, हमारा काम इस पैरामीटर के माध्यम से पारित अभिव्यक्ति के साथ कास्ट को इकाई संपत्ति (लाल रंग में चिह्नित पेड़ का हिस्सा) में बदलना है।

आइए कोशिश करें:

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

अब, हम संकलन चरण में «विधि नाम अपेक्षित» त्रुटि देख सकते हैं।

मुद्दा यह है कि एक अभिव्यक्ति एक वर्ग है जो प्रतिनिधि के बजाय एक वाक्यविन्यास पेड़ के नोड्स का प्रतिनिधित्व करता है और इसे सीधे नहीं कहा जा सकता है। अब, मुख्य कार्य किसी अन्य पैरामीटर को पास करते हुए एक व्यंजक बनाने का तरीका खोजना है।

आगंतुक

एक संक्षिप्त Google खोज के बाद, मुझे StackOverflow पर इसी तरह की समस्या का समाधान मिला।

अभिव्यक्तियों के साथ काम करने के लिए, ExpressionVisitor वर्ग है, जो विज़िटर पैटर्न का उपयोग करता है। यह सिंटैक्स ट्री को पार्स करने के क्रम में एक्सप्रेशन ट्री के सभी नोड्स को पार करने के लिए डिज़ाइन किया गया है और उन्हें संशोधित करने या इसके बजाय किसी अन्य नोड को वापस करने की अनुमति देता है। यदि न तो नोड और न ही उसके बच्चे के नोड बदले जाते हैं, तो मूल अभिव्यक्ति वापस आ जाती है।

ExpressionVisitor वर्ग से विरासत में मिलने पर, हम किसी भी ट्री नोड को एक्सप्रेशन से बदल सकते हैं, जिसे हम पैरामीटर से गुजरते हैं। इस प्रकार, हमें कुछ नोड-लेबल लगाने की आवश्यकता है, जिसे हम एक पैरामीटर के साथ ट्री में बदल देंगे। ऐसा करने के लिए, एक विस्तार विधि लिखें जो अभिव्यक्ति की कॉल का अनुकरण करेगी और एक मार्कर होगी।

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

अब, हम एक व्यंजक को दूसरे व्यंजक से बदल सकते हैं

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

एक विज़िटर लिखना आवश्यक है, जो कॉल मेथड को एक्सप्रेशन ट्री में इसके पैरामीटर से बदल देगा:

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

हम अपने मार्कर को बदल सकते हैं:

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

डिबगिंग में, हम देख सकते हैं कि अभिव्यक्ति वह नहीं है जिसकी हमें उम्मीद थी। फ़िल्टर में अभी भी आह्वान विधि शामिल है।

तथ्य यह है कि पैरामीटरगेटर और फाइनलफिल्टर एक्सप्रेशन दो अलग-अलग तर्कों का उपयोग करते हैं। इस प्रकार, हमें पैरामीटरगेटर में एक तर्क को अंतिम फ़िल्टर में तर्क के साथ बदलने की आवश्यकता है। ऐसा करने के लिए, हम एक और विज़िटर बनाते हैं:

परिणाम इस प्रकार है:

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

अब, सब कुछ वैसा ही काम करता है जैसा उसे करना चाहिए और हम, अंत में, अपनी निस्पंदन विधि लिख सकते हैं

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

निष्कर्ष

अभिव्यक्ति प्रतिस्थापन के साथ दृष्टिकोण का उपयोग न केवल फ़िल्टरिंग के लिए बल्कि सॉर्टिंग और डेटाबेस के लिए किसी भी क्वेरी के लिए भी किया जा सकता है।

साथ ही, यह विधि डेटाबेस में क्वेरी से अलग व्यावसायिक तर्क के साथ-साथ अभिव्यक्तियों को संग्रहीत करने की अनुमति देती है।

आप GitHub पर कोड देख सकते हैं।

यह लेख 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. केस एक्सप्रेशन के डर्टी सीक्रेट्स

  2. अपाचे निफ़ी

  3. शीर्ष 3 युक्तियाँ जिन्हें आपको तेज़ SQL दृश्य लिखने के लिए जानना आवश्यक है

  4. मूल्य द्वारा वस्तुओं की तुलना करना। भाग 6:संरचना समानता कार्यान्वयन

  5. डेटाबेस के लिए डिस्क स्थान की योजना