Django 2.1 - Custom Lookups

कस्टम लुकअप




django

कस्टम लुकअप

Django फ़िल्टरिंग के लिए अंतर्निहित लुकअप की एक विस्तृत विविधता प्रदान करता है (उदाहरण के लिए, exact और icontains )। यह दस्तावेज़ीकरण बताता है कि कस्टम लुकअप को कैसे लिखना है और मौजूदा लुकअप के काम को कैसे बदलना है। लुकअप API संदर्भों के लिए, लुकअप API संदर्भ देखें

एक साधारण खोज उदाहरण

चलो एक साधारण कस्टम लुकअप के साथ शुरू करते हैं। हम एक कस्टम लुकअप ne लिखेंगे जो exact विपरीत काम करता है। Author.objects.filter(name__ne='Jack') SQL में अनुवाद करेगा:

"author"."name" <> 'Jack'

यह एसक्यूएल स्वतंत्र है, इसलिए हमें विभिन्न डेटाबेस के बारे में चिंता करने की आवश्यकता नहीं है।

इस काम को करने के लिए दो चरण हैं। सबसे पहले हमें लुकअप को लागू करने की आवश्यकता है, फिर हमें Django को इसके बारे में बताने की आवश्यकता है। कार्यान्वयन काफी सीधा है:

from django.db.models import Lookup

class NotEqual(Lookup):
    lookup_name = 'ne'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s <> %s' % (lhs, rhs), params

NotEqual लुकअप को रजिस्टर करने के लिए हमें सिर्फ उस फील्ड क्लास पर register_lookup को कॉल करना होगा जिसे हम चाहते हैं कि लुकअप उपलब्ध हो। इस मामले में, लुकअप सभी Field उपवर्गों पर समझ में आता है, इसलिए हम इसे सीधे Field साथ पंजीकृत करते हैं:

from django.db.models.fields import Field
Field.register_lookup(NotEqual)

लुकअप पंजीकरण एक डेकोरेटर पैटर्न का उपयोग करके भी किया जा सकता है:

from django.db.models.fields import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...

अब हम किसी भी फील्ड foo लिए foo__ne उपयोग कर सकते हैं। आपको यह सुनिश्चित करने की आवश्यकता होगी कि यह पंजीकरण किसी भी क्वेरीसेट का उपयोग करने का प्रयास करने से पहले होता है। आप कार्यान्वयन को एक models.py फ़ाइल में रख सकते हैं, या AppConfig की ready() विधि में लुकअप को पंजीकृत कर सकते हैं।

कार्यान्वयन पर करीब से नज़र डालते हुए, पहली आवश्यक विशेषता lookup_name । यह ORM को यह समझने की अनुमति देता है कि SQL को उत्पन्न करने के लिए name__ne व्याख्या कैसे करें और NotEqual का उपयोग करें। अधिवेशन द्वारा, ये नाम हमेशा केवल अक्षरों से युक्त कम होते हैं, लेकिन एकमात्र कठिन आवश्यकता यह है कि इसमें स्ट्रिंग __ नहीं होना चाहिए।

हमें फिर as_sql विधि को परिभाषित करने की as_sql है। यह SQLCompiler ऑब्जेक्ट लेता है, जिसे compiler , और सक्रिय डेटाबेस कनेक्शन कहा जाता है। SQLCompiler ऑब्जेक्ट्स को प्रलेखित नहीं किया जाता है, लेकिन केवल एक चीज जो हमें उनके बारे में जानने की आवश्यकता है, वह यह है कि उनके पास एक compile() विधि है जो एक SQL स्ट्रिंग युक्त tuple लौटाती है, और पैरामीटर उस स्ट्रिंग में प्रक्षेपित होने के लिए। ज्यादातर मामलों में, आपको इसे सीधे उपयोग करने की आवश्यकता नहीं है और इसे process_lhs() और process_rhs() पर पास कर सकते हैं।

एक Lookup दो मूल्यों, lhs और rhs खिलाफ काम करता है, जो बाएं-हाथ की ओर और दाहिने-हाथ की ओर खड़ा है। बाएं हाथ का पक्ष आमतौर पर एक फ़ील्ड संदर्भ होता है, लेकिन यह क्वेरी एक्सप्रेशन API को लागू करने वाली कोई भी चीज़ हो सकती है । दायां हाथ उपयोगकर्ता द्वारा दिया गया मूल्य है। उदाहरण में Author.objects.filter(name__ne='Jack') , बाएँ हाथ की ओर Author मॉडल के name फ़ील्ड का संदर्भ है, और 'Jack' राइट-हैंड साइड है।

हम पहले बताए गए compiler ऑब्जेक्ट का उपयोग करके SQL के लिए आवश्यक मानों में परिवर्तित करने के लिए process_lhs और process_rhs को कॉल करते हैं। इन तरीकों से कुछ एसक्यूएल और उस एसक्यूएल में प्रक्षेपित होने वाले मापदंडों के साथ ट्यूपल्स वापस आ जाते हैं, जिस तरह हमें अपने as_sql विधि से वापस आने की आवश्यकता as_sql है। उपरोक्त उदाहरण में, process_lhs रिटर्न ('"author"."name"', []) और process_rhs रिटर्न ('"%s"', ['Jack']) । इस उदाहरण में बाएं हाथ की ओर के लिए कोई पैरामीटर नहीं थे, लेकिन यह हमारे पास मौजूद वस्तु पर निर्भर करेगा, इसलिए हमें अभी भी उन मापदंडों में शामिल करने की आवश्यकता है जिन्हें हम वापस करते हैं।

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

एक साधारण ट्रांसफार्मर उदाहरण

ऊपर दिया गया कस्टम लुकअप बहुत अच्छा है, लेकिन कुछ मामलों में आप एक साथ चेन लुकअप करने में सक्षम हो सकते हैं। उदाहरण के लिए, मान लें कि हम एक एप्लिकेशन बना रहे हैं, जहाँ हम abs() ऑपरेटर का उपयोग करना चाहते हैं। हमारे पास एक Experiment मॉडल है जो प्रारंभ मूल्य, अंतिम मूल्य और परिवर्तन (प्रारंभ - अंत) रिकॉर्ड करता है। हम उन सभी प्रयोगों को खोजना चाहते हैं जहाँ परिवर्तन एक निश्चित राशि के बराबर था ( Experiment.objects.filter(change__abs=27) ), या जहाँ यह एक निश्चित राशि से अधिक नहीं था ( Experiment.objects.filter(change__abs__lt=27) ) ।

ध्यान दें

यह उदाहरण कुछ हद तक वंचित है, लेकिन यह अच्छी तरह से कार्यक्षमता की सीमा को प्रदर्शित करता है जो डेटाबेस में स्वतंत्र तरीके से संभव है, और पहले से ही Django में कार्यक्षमता को दोहराए बिना।

हम एक AbsoluteValue ट्रांसफार्मर लिखकर शुरू करेंगे। यह तुलना करने से पहले मूल्य को बदलने के लिए SQL फ़ंक्शन ABS() का उपयोग करेगा:

from django.db.models import Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

अगला, इसे IntegerField लिए पंजीकृत करें:

from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)

अब हम अपने पहले वाले प्रश्नों को चला सकते हैं। Experiment.objects.filter(change__abs=27) निम्न SQL उत्पन्न करेगा:

SELECT ... WHERE ABS("experiments"."change") = 27

Lookup बजाय Transform का उपयोग करके इसका मतलब है कि हम आगे के लुकअप को चेन करने में सक्षम हैं। तो Experiment.objects.filter(change__abs__lt=27) निम्न SQL उत्पन्न करेगा:

SELECT ... WHERE ABS("experiments"."change") < 27

ध्यान दें कि यदि कोई अन्य लुकअप निर्दिष्ट नहीं है, तो Django change__abs=27 को change__abs__exact=27 रूप में व्याख्या change__abs=27

यह परिणाम को ORDER BY और DISTINCT ON खंड में उपयोग करने की अनुमति देता है। उदाहरण के लिए Experiment.objects.order_by('change__abs') उत्पन्न करता है:

SELECT ... ORDER BY ABS("experiments"."change") ASC

और खेतों पर विशिष्ट समर्थन करने वाले डेटाबेस (जैसे कि PostgreSQL), Experiment.objects.distinct('change__abs') उत्पन्न करता है:

SELECT ... DISTINCT ON ABS("experiments"."change")
Django 2.1 में परिवर्तित:

अंतिम दो पैराग्राफ में वर्णित आदेश और अलग समर्थन जोड़ा गया था।

जब Transform लागू होने के बाद लुकअप स्वीकार्य हैं, तो Django output_field विशेषता का उपयोग करता है। हमें इसे यहाँ निर्दिष्ट करने की आवश्यकता नहीं थी क्योंकि यह परिवर्तित नहीं हुआ था, लेकिन यह मानकर कि हम किसी क्षेत्र में AbsoluteValue को लागू कर रहे हैं जो अधिक जटिल प्रकार का प्रतिनिधित्व करता है (उदाहरण के लिए किसी मूल के सापेक्ष बिंदु, या एक जटिल संख्या) तो हमारे पास हो सकता है यह निर्दिष्ट करना चाहता था कि परिवर्तन आगे के लुकअप के लिए एक FloatField प्रकार लौटाता है। यह output_field एक output_field विशेषता जोड़कर किया जा सकता है:

from django.db.models import FloatField, Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

    @property
    def output_field(self):
        return FloatField()

यह सुनिश्चित करता है कि abs__lte तरह आगे के रूप में वे एक FloatField लिए व्यवहार करेंगे।

एक कुशल abs__lt लेखन लेखन

उपरोक्त लिखित abs लुकअप का उपयोग करते समय, उत्पादित एसक्यूएल कुछ मामलों में कुशलता से इंडेक्स का उपयोग नहीं करेगा। विशेष रूप से, जब हम change__abs__lt=27 उपयोग करते हैं, तो यह change__gt=-27 और change__lt=27 बराबर है। ( lte केस के लिए हम SQL BETWEEN उपयोग कर सकते हैं)।

तो हम निम्नलिखित SQL उत्पन्न करने के लिए Experiment.objects.filter(change__abs__lt=27) :

SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

कार्यान्वयन है:

from django.db.models import Lookup

class AbsoluteValueLessThan(Lookup):
    lookup_name = 'lt'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = compiler.compile(self.lhs.lhs)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params + lhs_params + rhs_params
        return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params

AbsoluteValue.register_lookup(AbsoluteValueLessThan)

कुछ उल्लेखनीय चीजें चल रही हैं। पहला, AbsoluteValueLessThan कॉल नहीं कर रहा है process_lhs() । इसके बजाय यह AbsoluteValue द्वारा किए गए lhs के परिवर्तन को छोड़ देता है और मूल lhs का उपयोग करता है। यही है, हम "experiments"."change" प्राप्त करना चाहते हैं "experiments"."change" नहीं ABS("experiments"."change") । सीधे self.lhs.lhs का जिक्र करना सुरक्षित है, क्योंकि AbsoluteValueLessThan को केवल AbsoluteValue लुकअप से एक्सेस किया जा सकता है, यही कारण है कि lhs हमेशा AbsoluteValue का एक उदाहरण है।

यह भी देखें कि चूंकि दोनों पक्षों का उपयोग क्वेरी में कई बार किया जाता है, इसलिए params को कई बार lhs_params और rhs_params समाहित करने की आवश्यकता होती है।

अंतिम क्वेरी सीधे डेटाबेस में उलटा ( 27 से -27 ) करती है। ऐसा करने का कारण यह है कि यदि self.rhs एक सादे पूर्णांक मान (उदाहरण के लिए एक F() संदर्भ) की तुलना में कुछ और है तो हम पायथन में परिवर्तन नहीं कर सकते हैं।

ध्यान दें

वास्तव में, __abs साथ अधिकांश लुकअप को इस तरह की श्रेणी के प्रश्नों के रूप में लागू किया जा सकता है, और अधिकांश डेटाबेस बैकएंड पर ऐसा करने के लिए अधिक समझदार होने की संभावना है क्योंकि आप अनुक्रमित का उपयोग कर सकते हैं। हालाँकि PostgreSQL के साथ आप abs(change) पर एक इंडेक्स जोड़ना चाह सकते हैं जो इन प्रश्नों को बहुत ही कुशल बनाने की अनुमति देगा।

एक द्विपक्षीय ट्रांसफार्मर उदाहरण

AbsoluteValue उदाहरण हमने पहले चर्चा की थी एक परिवर्तन है जो देखने के बाईं ओर लागू होता है। ऐसे कुछ मामले हो सकते हैं, जहाँ आप चाहते हैं कि परिवर्तन बाईं ओर और दाईं ओर दोनों पर लागू हो। उदाहरण के लिए, यदि आप किसी SQL फ़ंक्शन के लिए असंवेदनशील रूप से बाईं और दाईं ओर की समानता के आधार पर किसी क्वेरीसेट को फ़िल्टर करना चाहते हैं।

आइए यहां केस-असंवेदनशील परिवर्तन के सरल उदाहरण की जांच करें। यह परिवर्तन व्यवहार में बहुत उपयोगी नहीं है क्योंकि Django पहले से ही अंतर्निहित केस-इनसेटिव लुकअप का एक गुच्छा है, लेकिन यह डेटाबेस-अज्ञेय तरीके से द्विपक्षीय परिवर्तनों का एक अच्छा प्रदर्शन होगा।

हम एक UpperCase ट्रांसफार्मर को परिभाषित करते हैं जो तुलना करने से पहले मानों को बदलने के लिए SQL फ़ंक्शन UPPER() का उपयोग करता है। हम यह दर्शाने के लिए bilateral = True को परिभाषित करते हैं कि यह परिवर्तन दोनों lhs और rhs लागू होना चाहिए:

from django.db.models import Transform

class UpperCase(Transform):
    lookup_name = 'upper'
    function = 'UPPER'
    bilateral = True

अगला, इसे पंजीकृत करें:

from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)

अब, queryset Author.objects.filter(name__upper="doe") इस तरह से असंवेदनशील क्वेरी उत्पन्न करेगा:

SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

मौजूदा लुकअप के लिए वैकल्पिक कार्यान्वयन लिखना

कभी-कभी विभिन्न डेटाबेस विक्रेताओं को एक ही ऑपरेशन के लिए अलग SQL की आवश्यकता होती है। इस उदाहरण के लिए हम NotEqual ऑपरेटर के लिए MySQL के लिए एक कस्टम कार्यान्वयन फिर से लिखेंगे। इसके बजाय <> हम उपयोग करेंगे != ऑपरेटर। (ध्यान दें कि वास्तव में लगभग सभी डेटाबेस दोनों का समर्थन करते हैं, जिसमें Django द्वारा समर्थित सभी आधिकारिक डेटाबेस भी शामिल हैं)।

हम as_mysql विधि के साथ NotEqual उपवर्ग बनाकर एक विशिष्ट बैकएंड पर व्यवहार को बदल सकते हैं:

class MySQLNotEqual(NotEqual):
    def as_mysql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s != %s' % (lhs, rhs), params

Field.register_lookup(MySQLNotEqual)

फिर हम इसे Field पंजीकृत कर सकते हैं। यह मूल NotEqual वर्ग की जगह लेता है क्योंकि इसमें एक ही lookup_name

जब कोई क्वेरी संकलित की जाती है, तो Django पहले as_%s % connection.vendor विधियों की तलाश करता है, और फिर as_%s % connection.vendor वापस आ जाता है। इन-बिल्ट बैकेंड्स के लिए वेंडर नाम sqlite , postgresql , oracle और mysql

Django कैसे लुकअप और ट्रांसफॉर्म को निर्धारित करता है जो उपयोग किए जाते हैं

कुछ मामलों में आप डायनामिक रूप से बदलना चाह सकते हैं जो Transform या Lookup को ठीक करने के बजाय, पास किए गए नाम के आधार पर लौटाया जाता है। एक उदाहरण के रूप में, आपके पास एक ऐसा क्षेत्र हो सकता है जो निर्देशांक या एक मनमाना आयाम संग्रहीत करता है, और उन वस्तुओं को वापस करने के लिए एक सिंटैक्स की अनुमति देता है .filter(coords__x7=4) उन वस्तुओं को वापस करने के लिए जहां 7 वें समन्वय का मूल्य 4. है। कुछ के साथ get_lookup ओवरराइड कर get_lookup हैं:

class CoordinatesField(Field):
    def get_lookup(self, lookup_name):
        if lookup_name.startswith('x'):
            try:
                dimension = int(lookup_name[1:])
            except ValueError:
                pass
            else:
                return get_coordinate_lookup(dimension)
        return super().get_lookup(lookup_name)

आप तब Lookup उपवर्ग वापस करने के लिए उचित रूप से get_coordinate_lookup को परिभाषित करेंगे जो dimension के प्रासंगिक मूल्य को संभालता है।

इसी प्रकार की एक विधि है जिसका नाम get_transform()get_lookup() को हमेशा एक Lookup get_transform() लौटना चाहिए, और get_transform() एक Transform उपवर्ग। यह याद रखना महत्वपूर्ण है कि Transform ऑब्जेक्ट्स को आगे फ़िल्टर किया जा सकता है, और Lookup ऑब्जेक्ट्स नहीं कर सकते हैं।

फ़िल्टर करते समय, यदि केवल एक लुकअप नाम शेष है, तो हम Lookup तलाश करेंगे। यदि कई नाम हैं, तो यह एक Transform तलाश करेगा। ऐसी स्थिति में जहां केवल एक नाम और Lookup नहीं पाया जाता है, हम एक Transform और फिर उस Transform पर exact लुकअप करते हैं। सभी कॉल अनुक्रम हमेशा एक Lookup साथ समाप्त होते हैं। स्पष्टीकरण देना:

  • .filter(myfield__mylookup) myfield.get_lookup('mylookup') को कॉल करेगा।
  • .filter(myfield__mytransform__mylookup) myfield.get_transform('mytransform') .filter(myfield__mytransform__mylookup) को कॉल करेगा, और फिर mytransform.get_lookup('mylookup')
  • .filter(myfield__mytransform) पहले myfield.get_lookup('mytransform') कॉल करेगा, जो विफल हो जाएगा, इसलिए यह myfield.get_transform('mytransform') और फिर mytransform.get_lookup('exact') को कॉल करने पर वापस आ जाएगी।