Djangoを読む

はじめに

前回はobjects.all()について見ました。条件がない場合でもかなり長かったですがおかげで処理フローについてはもう見なくていいかなという感じです。

というわけで、今回は条件指定、filterを呼び出した時(と、それに対応したSQL構築)を見ていきましょう。具体的には、

>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

について見ていきます。

QuerySet

というわけで例によってQuerySetクラスから開始です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
-
|
|
!
 
    def filter(self, *args, **kwargs):
        """
        Returns a new QuerySet instance with the args ANDed to the existing
        set.
        """
        return self._filter_or_exclude(False, *args, **kwargs)

すぐ下にはexcludeが書いてあって、ほぼ同じ処理なので共通化しているようですね。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
 
 
 
 
 
 
    def _filter_or_exclude(self, negate, *args, **kwargs):
        clone = self._clone()
        if negate:
            clone.query.add_q(~Q(*args, **kwargs))
        else:
            clone.query.add_q(Q(*args, **kwargs))
        return clone

Q。非常に怪しげです(笑)

Qクラスはdjango/db/models/query_utils.pyに書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 
-
|
|
!
-
!
 
 
 
 
 
class Q(tree.Node):
    """
    Encapsulates filters as objects that can then be combined logically (using
    `&` and `|`).
    """
    # Connection types
    AND = 'AND'
    OR = 'OR'
    default = AND
 
    def __init__(self, *args, **kwargs):
        super(Q, self).__init__(children=list(args) + list(kwargs.items()))

treeモジュールはdjango.utilsにあります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
-
|
|
|
!
 
 
-
|
|
!
 
 
 
class Node(object):
    """
    A single internal node in the tree graph. A Node should be viewed as a
    connection (the root) with the children being either leaf nodes or other
    Node instances.
    """
 
    def __init__(self, children=None, connector=None, negated=False):
        """
        Constructs a new Node. If no connector is given, the default will be
        used.
        """
        self.children = children[:] if children else []
        self.connector = connector or self.default
        self.negated = negated

普通のツリーのノードです。

Query

というわけで、Qオブジェクトが何者なのかわかったのでQueryクラスに移りましょう。add_qメソッド

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 
-
|
|
!
-
|
|
|
|
|
!
 
 
 
 
 
    def add_q(self, q_object):
        """
        A preprocessor for the internal _add_q(). Responsible for doing final
        join promotion.
        """
        # For join promotion this case is doing an AND for the added q_object
        # and existing conditions. So, any existing inner join forces the join
        # type to remain inner. Existing outer joins can however be demoted.
        # (Consider case where rel_a is LOUTER and rel_a__col=1 is added - if
        # rel_a doesn't produce any rows, then the whole condition must fail.
        # So, demotion is OK.
        existing_inner = set(
            (a for a in self.alias_map if self.alias_map[a].join_type == INNER))
        clause, _ = self._add_q(q_object, self.used_aliases)
        if clause:
            self.where.add(clause, AND)
        self.demote_joins(existing_inner)

alias_mapは前回も出てきましたが、今の状況では空なはずなので無視。 また、used_aliasesも現時点では空です。

_add_qに進む前に、whereが何者なのか確認しておきます。Queryクラスの__init__メソッド

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
-
!
 
    def __init__(self, model, where=WhereNode):
        # 省略
        self.where = where()
        self.where_class = where

WhereNodeはdjango/db/models/sql/where.pyに定義されています。

Everything is expanded.Everything is shortened.
  1
 
class WhereNode(tree.Node):

というわけで、WHEREもツリーとして表されているようです。

_add_q

では改めて_add_qメソッド

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
    def _add_q(self, q_object, used_aliases, branch_negated=False,
               current_negated=False, allow_joins=True, split_subq=True):
        """
        Adds a Q-object to the current filter.
        """
        connector = q_object.connector
        current_negated = current_negated ^ q_object.negated
        branch_negated = branch_negated or q_object.negated
        target_clause = self.where_class(connector=connector,
                                         negated=q_object.negated)
        joinpromoter = JoinPromoter(q_object.connector, len(q_object.children), current_negated)
        for child in q_object.children:
            if isinstance(child, Node):
                child_clause, needed_inner = self._add_q(
                    child, used_aliases, branch_negated,
                    current_negated, allow_joins, split_subq)
                joinpromoter.add_votes(needed_inner)
            else:
                child_clause, needed_inner = self.build_filter(
                    child, can_reuse=used_aliases, branch_negated=branch_negated,
                    current_negated=current_negated, connector=connector,
                    allow_joins=allow_joins, split_subq=split_subq,
                )
                joinpromoter.add_votes(needed_inner)
            if child_clause:
                target_clause.add(child_clause, connector)
        needed_inner = joinpromoter.update_join_types(self)
        return target_clause, needed_inner

うーん、いきなりややこしくなった。とりあえずnegatedと名のつくものについては、今回の場合は全部Falseです。 次にJoinPromoter、今回のケースではJOINは起こらないので無視してしまっていいでしょう。 というわけで、ポイントとなるのは以下になります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 
 
-
!
 
 
 
 
 
 
 
 
        for child in q_object.children:
            if isinstance(child, Node):
                # こっちじゃない
            else:
                child_clause, needed_inner = self.build_filter(
                    child, can_reuse=used_aliases, branch_negated=branch_negated,
                    current_negated=current_negated, connector=connector,
                    allow_joins=allow_joins, split_subq=split_subq,
                )
                joinpromoter.add_votes(needed_inner)
            if child_clause:
                target_clause.add(child_clause, connector)

build_filterメソッドを呼んでる引数を確認しておくと次の通りです。

child
('question_text__startswith', 'What')
can_reuse
set()
branch_negated
False
current_negated
False
connector
'AND'
allow_joins
True
split_subq
True

build_filter

build_filterは長いのでところどころ省略

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 
 
 
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
    def build_filter(self, filter_expr, branch_negated=False, current_negated=False,
                     can_reuse=None, connector=AND, allow_joins=True, split_subq=True):
        arg, value = filter_expr
        lookups, parts, reffed_expression = self.solve_lookup_type(arg)
 
        # Work out the lookup type and remove it from the end of 'parts',
        # if necessary.
        value, lookups, used_joins = self.prepare_lookup_value(value, lookups, can_reuse, allow_joins)
 
        clause = self.where_class()
 
        opts = self.get_meta()
        alias = self.get_initial_alias()
        allow_many = not branch_negated or not split_subq
 
        try:
            field, sources, opts, join_list, path = self.setup_joins(
                parts, opts, alias, can_reuse=can_reuse, allow_many=allow_many)
        except MultiJoin as e:
            # 省略
 
        if can_reuse is not None:
            can_reuse.update(join_list)
        used_joins = set(used_joins).union(set(join_list))
        targets, alias, join_list = self.trim_joins(sources, join_list, path)
 
        if field.is_relation:
            # 省略
        else:
            col = targets[0].get_col(alias, field)
            condition = self.build_lookup(lookups, col, value)
            lookup_type = condition.lookup_name
 
        clause.add(condition, AND)
 
        require_outer = lookup_type == 'isnull' and value is True and not current_negated
        return clause, used_joins if not require_outer else ()

filterでは関連オブジェクトのフィールドが○○の値、という条件も書けるので実際にはJOINが起こらないような場合でもJOINが起こるかも、という前提で処理が行われるようです。上記のうち、掘り下げた方がよさそうなのは次の5メソッドです。

  • solve_lookup_type
  • prepare_lookup_value
  • setup_joins
  • trim_joins
  • build_lookup

solve_lookup_type

ではまずsolve_lookup_typeです。一部省略

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
!
 
 
 
 
 
 
    def solve_lookup_type(self, lookup):
        """
        Solve the lookup type from the lookup (eg: 'foobar__id__icontains')
        """
        lookup_splitted = lookup.split(LOOKUP_SEP)
        _, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
        field_parts = lookup_splitted[0:len(lookup_splitted) - len(lookup_parts)]
        if len(lookup_parts) == 0:
            lookup_parts = ['exact']
        return lookup_parts, field_parts, False

LOOKUP_SEPは'__'、渡されているのは'question_text__startswith'なので、['question_text', 'startswith']と分離されます。

で、下請けのnames_to_pathが呼ばれるわけですがこれがまた長いので通るところの要点だけ抽出

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
 
 
 
 
 
 
-
!
 
-
!
-
!
 
 
 
    def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):
        """
        Walks the list of names and turns them into PathInfo tuples. Note that
        a single name in 'names' can generate multiple PathInfos (m2m for
        example).
 
        'names' is the path of names to travel, 'opts' is the model Options we
        start the name resolving from, 'allow_many' is as for setup_joins().
        If fail_on_missing is set to True, then a name that can't be resolved
        will generate a FieldError.
 
        Returns a list of PathInfo tuples. In addition returns the final field
        (the last used join field), and target (which is a field guaranteed to
        contain the same value as the final field). Finally, the method returns
        those names that weren't found (which are likely transforms and the
        final lookup).
        """
        path, names_with_path = [], []
        for pos, name in enumerate(names):
            field = None
            try:
                field = opts.get_field(name)
            except FieldDoesNotExist:
                # 省略
 
            if hasattr(field, 'get_path_info'):
                # 省略
            else:
                # Local non-relational field.
                final_field = field
                targets = (field,)
                break
        return path, final_field, targets, names[pos + 1:]

関連オブジェクトのフィールドを参照しない場合は結局これだけです。solve_lookup_typeでは結局戻り値の4つ目しか使わず、['startswith']と検索条件が分離されて返されることになります。最終的にsolve_lookup_typeは、

['startswith'], ['question_text'], False

という情報を返すことになります。

prepare_lookup_value

次にprepare_lookup_valueです。いつも通り一部省略

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
-
!
-
|
!
 
 
 
 
 
    def prepare_lookup_value(self, value, lookups, can_reuse, allow_joins=True):
        # Default lookup if none given is exact.
        used_joins = []
        # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all
        # uses of None as a query value.
        if value is None:
            if lookups[-1] not in ('exact', 'iexact'):
                raise ValueError("Cannot use None as a query value")
            lookups[-1] = 'isnull'
            value = True
        return value, lookups, used_joins

valueがNoneの時はSQLをIS NULLにする処理が行われていますが、それ以外をしている場合はそのまま返されているだけです(実際にはもう少しprepareな処理がされています)。つまり、

'What', ['startswith'], []

という情報が返されます。

setup_joins

setup_joinsが呼ばれる前に、前回も出てきたget_initial_aliasメソッドが呼ばれています。つまり、検索のベースとなるテーブルはすでに設定された状態になっています。

で、setup_joins。とは言うものの、今回はJOINに関係しているところは無視します(実際、for文は回りません)。メソッド説明はちょっと長いけどそのまま掲載

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
 
-
!
 
 
-
|
|
!
-
!
    def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True):
        """
        Compute the necessary table joins for the passage through the fields
        given in 'names'. 'opts' is the Options class for the current model
        (which gives the table we are starting from), 'alias' is the alias for
        the table to start the joining from.
 
        The 'can_reuse' defines the reverse foreign key joins we can reuse. It
        can be None in which case all joins are reusable or a set of aliases
        that can be reused. Note that non-reverse foreign keys are always
        reusable when using setup_joins().
 
        If 'allow_many' is False, then any reverse foreign key seen will
        generate a MultiJoin exception.
 
        Returns the final field involved in the joins, the target field (used
        for any 'where' constraint), the final 'opts' value, the joins and the
        field path travelled to generate the joins.
 
        The target field is the field containing the concrete value. Final
        field can be something different, for example foreign key pointing to
        that value. Final field is needed for example in some value
        conversions (convert 'obj' in fk__id=obj to pk val using the foreign
        key field for example).
        """
        joins = [alias]
        # First, generate the path for the names
        path, final_field, targets, rest = self.names_to_path(
            names, opts, allow_many, fail_on_missing=True)
 
        # Then, add the path to the query's joins. Note that we can't trim
        # joins at this stage - we will need the information about join type
        # of the trimmed joins.
        for join in path:
            # 省略
        return final_field, targets, opts, joins, path

結局、names_to_pathの戻り値がほぼそのまま返されます。

question_textのCharField, (question_textのCharField,), QuestionのOptions, [QuestionのBaseTable], []

trim_joins

trim_joins。同じくfor文は無視します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
-
|
|
|
|
|
|
|
|
|
|
|
!
 
 
-
!
    def trim_joins(self, targets, joins, path):
        """
        The 'target' parameter is the final field being joined to, 'joins'
        is the full list of join aliases. The 'path' contain the PathInfos
        used to create the joins.
 
        Returns the final target field and table alias and the new active
        joins.
 
        We will always trim any direct join if we have the target column
        available already in the previous table. Reverse joins can't be
        trimmed as we don't know if there is anything on the other side of
        the join.
        """
        joins = joins[:]
        for pos, info in enumerate(reversed(path)):
            # 省略
        return targets, joins[-1], joins
(question_textのCharField,), QuestionのBaseTable, [QuestionのBaseTable]

が返されます。

build_lookup

最後にbuild_lookupです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
-
|
|
|
|
|
|
!
 
 
 
-
|
|
!
 
 
-
|
|
!
 
 
 
 
    def build_lookup(self, lookups, lhs, rhs):
        """
        Tries to extract transforms and lookup from given lhs.
 
        The lhs value is something that works like SQLExpression.
        The rhs value is what the lookup is going to compare against.
        The lookups is a list of names to extract using get_lookup()
        and get_transform().
        """
        lookups = lookups[:]
        while lookups:
            name = lookups[0]
            # If there is just one part left, try first get_lookup() so
            # that if the lhs supports both transform and lookup for the
            # name, then lookup will be picked.
            if len(lookups) == 1:
                final_lookup = lhs.get_lookup(name)
                if not final_lookup:
                    # We didn't find a lookup. We are going to interpret
                    # the name as transform, and do an Exact lookup against
                    # it.
                    lhs = self.try_transform(lhs, name, lookups)
                    final_lookup = lhs.get_lookup('exact')
                return final_lookup(lhs, rhs)
            lhs = self.try_transform(lhs, name, lookups)
            lookups = lookups[1:]

lhsはCharFieldなのでとget_lookupを探しても定義はありません。が、

Everything is expanded.Everything is shortened.
  1
 
class Field(RegisterLookupMixin):

というわけでこちらに書いてありそうです。query_utilsモジュールなので見てみると、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
-
!
 
    def get_lookup(self, lookup_name):
        from django.db.models.lookups import Lookup
        found = self._get_lookup(lookup_name)
        if found is None and hasattr(self, 'output_field'):
            return self.output_field.get_lookup(lookup_name)
        if found is not None and not issubclass(found, Lookup):
            return None
        return found
 
    def _get_lookup(self, lookup_name):
        try:
            return self.class_lookups[lookup_name]
        except KeyError:
            # 省略
        except AttributeError:
            # This class didn't have any class_lookups
            pass
        return None

class_lookupsへの登録はクラスメソッドとして定義されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
 
 
 
 
    @classmethod
    def register_lookup(cls, lookup, lookup_name=None):
        if lookup_name is None:
            lookup_name = lookup.lookup_name
        if 'class_lookups' not in cls.__dict__:
            cls.class_lookups = {}
        cls.class_lookups[lookup_name] = lookup
        return lookup

次の疑問は、「仕組みはわかったけど、じゃあいつlookupが登録されるの?」ということですが、Lookupクラスがインポートされるときのようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
 
 
 
 
 
 
 
 
 
class StartsWith(PatternLookup):
    lookup_name = 'startswith'
    prepare_rhs = False
 
    def process_rhs(self, qn, connection):
        rhs, params = super(StartsWith, self).process_rhs(qn, connection)
        if params and not self.bilateral_transforms:
            params[0] = "%s%%" % connection.ops.prep_for_like_query(params[0])
        return rhs, params
Field.register_lookup(StartsWith)

というわけで、startswithに対応するLookupが登録されます。 で、StartsWithオブジェクトが作られてようやくfilterメソッド呼び出しから始まるオブジェクト構築が完了のようです。

構築されたStartsWithオブジェクトはbuild_filter→_add_q→add_qと戻る過程で毎回メソッドローカルのWhereNodeにaddされていますが、多段になるわけではなく以下のようにself.where直下にStartsWithが来ることになります。

self.where
  StartsWith(lhs=question_textのCol, rhs='What')

WhereNode

オブジェクトが構築できたら、後は前回見たように処理が行われ、self.whereのcompileが行われます。つまり、as_sqlが呼ばれます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
-
|
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
-
|
|
!
 
 
 
    def as_sql(self, compiler, connection):
        """
        Returns the SQL version of the where clause and the value to be
        substituted in. Returns '', [] if this node matches everything,
        None, [] if this node is empty, and raises EmptyResultSet if this
        node can't match anything.
        """
        result = []
        result_params = []
        if self.connector == AND:
            full_needed, empty_needed = len(self.children), 1
        else:
            full_needed, empty_needed = 1, len(self.children)
 
        for child in self.children:
            try:
                sql, params = compiler.compile(child)
            except EmptyResultSet:
                empty_needed -= 1
            else:
                if sql:
                    result.append(sql)
                    result_params.extend(params)
                else:
                    full_needed -= 1
            # 省略
        conn = ' %s ' % self.connector
        sql_string = conn.join(result)
        if sql_string:
            if self.negated:
                # Some backends (Oracle at least) need parentheses
                # around the inner SQL in the negated case, even if the
                # inner SQL contains just a single expression.
                sql_string = 'NOT (%s)' % sql_string
            elif len(result) > 1:
                sql_string = '(%s)' % sql_string
        return sql_string, result_params

子ノードをcompileしてつなげているだけです。

StartsWith

では子ノードのStartsWithについて見ていきましょう。まずは継承関係

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
 
 
 
class StartsWith(PatternLookup):
 
class PatternLookup(BuiltinLookup):
 
class BuiltinLookup(Lookup):

BuildinLookupまで行くとas_sqlメソッドが書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
 
 
 
    def as_sql(self, compiler, connection):
        lhs_sql, params = self.process_lhs(compiler, connection)
        rhs_sql, rhs_params = self.process_rhs(compiler, connection)
        params.extend(rhs_params)
        rhs_sql = self.get_rhs_op(connection, rhs_sql)
        return '%s %s' % (lhs_sql, rhs_sql), params

process_lhsはas_sqlのすぐ上に書かれています。DBMSに応じた処理がされていますが結局のところ、Colオブジェクトがcompileされて'"polls_question"."question_text"'が得られます。

process_rhs。StartsWithでオーバーライドされています。bilateral_transformsは空リストなはず→結論を先取りしますがparamsは['What']なので%が足されて['What%']、つまり、前方一致になります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
 
 
 
    def process_rhs(self, qn, connection):
        rhs, params = super(StartsWith, self).process_rhs(qn, connection)
        if params and not self.bilateral_transforms:
            params[0] = "%s%%" % connection.ops.prep_for_like_query(params[0])
        return rhs, params

で親クラス、Lookupの方のprocess_rhs。ただの文字列なのでget_db_prep_lookupが呼ばれます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 
 
-
|
|
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 
 
    def process_rhs(self, compiler, connection):
        value = self.rhs
        if self.bilateral_transforms:
            # 省略
        # Due to historical reasons there are a couple of different
        # ways to produce sql here. get_compiler is likely a Query
        # instance, _as_sql QuerySet and as_sql just something with
        # as_sql. Finally the value can of course be just plain
        # Python value.
        if hasattr(value, 'get_compiler'):
            value = value.get_compiler(connection=connection)
        if hasattr(value, 'as_sql'):
            sql, params = compiler.compile(value)
            return '(' + sql + ')', params
        if hasattr(value, '_as_sql'):
            sql, params = value._as_sql(connection=connection)
            return '(' + sql + ')', params
        else:
            return self.get_db_prep_lookup(value, connection)
 
    def get_db_prep_lookup(self, value, connection):
        return ('%s', [value])

get_rhs_opはPatternLookupにも書かれていますがBuiltinLookup(親クラス)のメソッドに流れるはず。

Everything is expanded.Everything is shortened.
  1
  2
 
 
    def get_rhs_op(self, connection, rhs):
        return connection.operators[self.lookup_name] % rhs

sqlite3の場合、operatorsは以下の通りです。つまり、LIKEを使って前方一致が実現されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
    operators = {
        'exact': '= %s',
        'iexact': "LIKE %s ESCAPE '\\'",
        'contains': "LIKE %s ESCAPE '\\'",
        'icontains': "LIKE %s ESCAPE '\\'",
        'regex': 'REGEXP %s',
        'iregex': "REGEXP '(?i)' || %s",
        'gt': '> %s',
        'gte': '>= %s',
        'lt': '< %s',
        'lte': '<= %s',
        'startswith': "LIKE %s ESCAPE '\\'",
        'endswith': "LIKE %s ESCAPE '\\'",
        'istartswith': "LIKE %s ESCAPE '\\'",
        'iendswith': "LIKE %s ESCAPE '\\'",
    }

最終的に、WhereNodeがコンパイルされると以下の文字列(とパラメータリスト)になります。

'"polls_question"."question_text" LIKE %s ESCAPE \'\\\'', ['What%']

おわりに

今回はfilterメソッドを使い検索条件が指定された時の処理を見てきました。前回に引き続き、記述をオブジェクトとして表現するということが徹底されていて相当難解になっていました。一度オブジェクトが構築されればcompileについては機械的な変換です。

今回(と前回)、JOINについては実際には発生しないのでばっさり省略していますが、この部分もちゃんと見ないとなという感想です。とりあえずその前に、次回はモデルの逆参照(参照されてる側のモデルが集約してるモデルへのアクセス経路を持つ)について見ていく予定です。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-08-15 (火) 17:27:52 (2438d)