How to write custom lookups

django는 필터링(예: “exeact” 및 “icontains”)을 위한 다양한 ref:’내장 검색’을 제공한다. 이 문서에서는 사용자 정의 룩업을 작성하는 방법과 기존 조회의 작업을 변경하는 방법에 대해 설명합니다. 조회에 대한 API 참조는 :doc:’/ref/models/lookups’를 참조하십시오.

조회 예제

소규모 사용자 맞춤 조회부터 시작하겠습니다. 우리는 “exact”와 상반되는 맞춤형 조회 “ne”을 작성할 것이다. “Author.objects.filter(name__ne = ‘Jack’)는 SQL로 변환됩니다.

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

이 SQL은 백엔드에 독립적이어서 다른 데이터베이스에 대해서 염려를 안해도 됩니다.

이 작업을 수행하는 데는 두 가지 단계가 있습니다. 먼저 조회를 구현한 후 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 import Field
Field.register_lookup(NotEqual)

decorator 패턴을 사용하여 조회 등록을 수행 할 수 있습니다.

from django.db.models import Field

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

이제 우리는 ``foo_ne”를 어떤 “foo” 의 어떠한 필드에서도 사용할 수 있다. 이 등록을 사용하여 쿼리 세트를 만들기 전에 이 등록이 수행되는지 확인해야 합니다. 당신은 그 실행을 ``models.py”파일 안에 구현할 수 있고, “AppConfig”안의 “ready()함수를 사용하여 조회를 등록할 수 있다.

그 구현을 자세히 들여다보면, 가장 먼저 요구되는 속성은 ``lookup_name”이다. 이를 통해 ORM은 ``name__ne”의 해석 방법을 이해하고 “Not Equal”을 사용하여 SQL을 생성할 수 있다. 관례적으로, 이 이름들은 항상 소문자 문자열로 문자만 포함하고 있지만, 유일한 어려운 요구조건은 문자열 “__”를 포함해서는 안 된다는 것이다.

그다음 우리는 ``as_sql” 방식을 정의할 필요가 있다. 이것은”compiler”라고 불리는 ``SQLCompiler”객체와 활성화된 데이터베이스 연결이 필요하다. “SQL Compiler” 객체는 문서화되어 있지 않지만 그것들에 대해 우리가 알아야 할 것은 SQL 문자열이 들어 있는 튜플을 반환하는 ``compile()” 방법과 그 문자열에 삽입될 파라미터가 있다는 것뿐이다. 대부분의 경우 직접 사용할 필요가 없으며 ``process_lhs()”와 ``process_rhs()”로 직접전달할 수 있다.

“Lookup”은 “lhs” 와 “rhs”라는 값에서 동작하는데, 각각 왼쪽과 오른쪽을 뜻한다. 왼쪽은 대부분 필드 레퍼런스이지만, :ref:’쿼리 표현 API’ 를 구현하는 모든 것이 될 수 있습니다. 오른쪽 값은 유저에 의해서 정해진다. “Author.objects.filter(name__ne=’Jack)” 예시에서, 왼쪽은 “Author” 모델의 “name” 필드의 레퍼런스이고, 오른쪽은 “Jack”이다.

“process_lhs”와 “process_rhs”는 앞에서 설명한 “compiler” 개체를 사용하여 SQL에 필요한 값으로 변환하기 위해 “process_lhs”와 “process_rhs”라고 부른다. 이러한 방법들은 우리가 우리의 “as_sql” 방법에서 돌아올 때처럼 일부 SQL을 포함하는 튜플과 그 SQL에 보간될 파라미터를 반환한다. 위의 예에서 “process_lhs”는 ``(‘author’.name”, []), “process_rhs”는 “(‘”%s”’, [‘Jack’])”을 반환합니다.”이 예에서는 좌익에 대한 매개변수가 없었지만 이것은 우리가 가지고 있는 대상에 따라 달라지므로 우리는 여전히 그것들을 반환하는 매개변수에 포함시킬 필요가 있다.

마지막으로 부품을 SQL 표현식과 “<>”로 결합하고 질의에 대한 모든 매개 변수를 제공한다. 그런 다음 생성된 SQL 문자열과 매개 변수가 포함된 튜플을 반환합니다.

변압기 예제

위의 사용자 지정 조회는 훌륭하지만 경우에 따라 조회를 함께 연결할 수 있습니다. 예를 들어, 우리가 “abs()” 연산자를 사용하고자 하는 응용 프로그램을 만들고 있다고 가정해 보자. 우리는 시작값, 끝값, 변화(시작-끝)를 기록하는 “Experimental” 모델을 가지고 있다. 우리는 그 변화가 일정량과 동일한 모든 실험(“Experiment.objects.filter(change__abs =27’) 또는 일정량을 초과하지 않는 (“Experiment.objects.filter(change__abs__lt |27)”)을 찾고자 한다.

주석

이 예제는 다소 작위적이지만, 이미 Django에 있는 기능을 복제하지 않고 데이터베이스 백엔드에 독립적인 방식으로 가능한 기능의 범위를 잘 보여준다.

“우리는 ‘Absolute’ 변압기를 쓰는 것으로 시작할 것이다. 이는 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

By using Transform instead of Lookup it means we are able to chain further lookups afterward. So Experiment.objects.filter(change__abs__lt=27) will generate the following SQL:

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

다른 조회가 지정되지 않은 경우, Django는 “change__abs abs=27”을 “change__abs_exact=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")

“Transform”이 적용된 후 어떤 룩업이 허용되는지를 찾을 때, Django는 “output_field” 속성을 사용한다. 변화가 되지 않았기 때문에 여기에 명시할 필요는 없지만, 보다 복잡한 유형(예:기원에 상대적인 점, 또는 복합적인 수)을 나타내는 어떤 분야에 “AbsoluteValue”을 적용한다고 가정하면 변환이 추가 조회를 위해 “FloatField” 타입을 반환한다고 명시하고 싶었을 수도 있다. 이는 “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” 룩업을 사용할 때 생성된 SQL은 경우에 따라 인덱스를 효율적으로 사용하지 않을 것이다. 특히 우리가 ``change_abs__lt=27”을 사용할 때 이는 ``change_gt =-27”과 “change_lt =27”에 해당한다(“lte”케이스에서는 우리는 SQL “BETWEEN”을 사용할 수 있다).

그래서 우리는 “Experiment.objects.filter(change__abs__lt=27)”을 다음 SQL문을 생성하기를 원한다:

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”을 사용한다. 즉, 우리는 “ABS(“experiments”.”change”)”가 아니라 “experiments””를 원한다. ``self.lhs.lhs”를 직접 지칭하는 것은 ``AbsouleValueLessThan”이라고 해도 무방하다.Than’은 ``AbsoluteValue” 조회에서만 접근할 수 있는데 그것은 ``lhs”은 항상 ``AbsoluteValue”의 한 예이다.

또한 양쪽이 쿼리에서 여러 번 사용되므로 “lhs_params”와 “rhs_params”를 여러 번 포함해야 한다.

마지막 쿼리는 데이터베이스에서 직접 반전(“27”to “-27”)을 수행합니다. 그 이유는 “self.rhs”가 단순한 정수 값(예: “F() reference)이 아니면 파이썬으로 변환을 할 수 없기 때문이다.

주석

실제로 ``__abs”가 있는 대부분의 조회는 이와 같은 범위 쿼리로 구현될 수 있으며 대부분의 데이터베이스 백엔드에서는 인덱스를 사용할 수 있으므로 그렇게 하는 것이 더 현명할 것이다. 그러나 Postgre와 함께SQL ``abs(change)”에 인덱스를 추가할 수 있고 이러한 쿼리들은 매우 효율적일 것이다.

쌍방향 변압기 예제

앞에서 논의한 “AbsoluteValue”의 예는 룩업의 왼쪽에 적용되는 변화이다. 변환을 왼쪽과 오른쪽 모두에 적용하려는 경우가 있을 수 있습니다. 예를 들어 일부 SQL 함수에 대해 왼쪽과 오른쪽의 동일성을 기준으로 쿼리 세트를 필터링하려는 경우

여기서는 대소문자를 구분하지 않는 변환을 살펴보겠습니다. 이러한 변환은 Django가 이미 여러 개의 대소문자를 구분하지 않는 검색 기능을 제공하므로 실제로 유용하지는 않습니다. 그러나 이는 데이터베이스에 구애받지 않는 방식으로 상호 변환의 좋은 예가 될 것입니다.

우리는 SQL 함수 “UPPER()”를 사용하여 비교 전에 값을 변환하는 “UpperCase” 변압기를 정의한다. 우리는 다음과 같이 정의한다. 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)

이제 ``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, **extra_context):
        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”에 등록할 수 있다. 그것은 동일한 “lookup_name”을 가지고 있기 때문에 원래의 “Not Equal” 클래스를 대신한다.

쿼리를 컴파일할 때, Django는 먼저 “as_%s” 연결을 찾는다.공급업체는 방법을 채택한 다음 다시 “as_sqlp”로 돌아간다. 내장된 백엔드의 벤더 이름은 “sqlite”, “postgresql”, “oracle” 및 “mysql”이다.

Django가 사용되는 룩업 및 변환을 결정하는 방법

어떤 경우에는 “Transform” 또는 “Lookup”이 반환되는 것을 고치는 것이 아니라 그 이름에 따라 동적으로 바꾸기를 원할 수도 있다. 예를 들어 좌표나 임의 차원을 저장하는 필드를 사용할 수 있으며,``.filter(coords__x7=4)``와 같은 구문이 7번째 좌표 값이 4인 객체를 반환하도록 허용할 수 있습니다. 이를 위해 당신은 다음과 같은 것으로 ``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)

그런 다음 “get_cordinate_lookup”을 적절히 정의하여 “dimension”의 관련 가치를 다루는 ``Lookup” 하위 클래스를 반환하게 된다.

``get_transform()”” ``get_lookup()””은 항상 ``Lookup” 하위 클래스를 반환하고 ``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’)``, 을 호출하고 ``mytransform.get_lookup(‘mylookup’)``을 호출할 것이다.
  • .filter(myfield__mytransform) 는 우선 myfield.get_lookup('mytransform'), 을 호출하는데 실패할것이고, 그 후 myfield.get_transform('mytransform') 을 호출한 다음 ``mytransform.get_lookup(‘exact’)``을 호출할 것이다.
Back to Top