Ticket #5420: lazy.diff

File lazy.diff, 48.8 KB (added by Jesse Young, 16 years ago)

implementation of lazy fields

  • django/db/models/sql/query.py

     
    5050        self.join_map = {}
    5151        self.rev_join_map = {}  # Reverse of join_map.
    5252        self.quote_cache = {}
    53         self.default_cols = True
    5453        self.default_ordering = True
    5554        self.standard_ordering = True
    5655        self.ordering_aliases = []
     
    7372        self.distinct = False
    7473        self.select_related = False
    7574        self.related_select_cols = []
     75        self.custom_fields = False       
    7676
    7777        # Arbitrary maximum limit for select_related. Prevents infinite
    7878        # recursion. Can be changed by the depth parameter to select_related().
     
    162162        obj.join_map = self.join_map.copy()
    163163        obj.rev_join_map = self.rev_join_map.copy()
    164164        obj.quote_cache = {}
    165         obj.default_cols = self.default_cols
    166165        obj.default_ordering = self.default_ordering
    167166        obj.standard_ordering = self.standard_ordering
    168167        obj.ordering_aliases = []
     
    181180        obj.distinct = self.distinct
    182181        obj.select_related = self.select_related
    183182        obj.related_select_cols = []
     183        obj.custom_fields = self.custom_fields       
    184184        obj.max_depth = self.max_depth
    185185        obj.extra_select = self.extra_select.copy()
    186186        obj.extra_tables = self.extra_tables
     
    197197            obj._setup_query()
    198198        return obj
    199199
     200    def get_base_fields(self):
     201        """
     202        Returns a list of fields on the base object (i.e., not related objects)
     203        that are retrieved by this query. In the result row, these columns are
     204        row[len(self.extra_select):len(self.extra_select)+len(self.get_base_fields())]
     205        """
     206        if self.select_fields:
     207            return self.select_fields
     208        else:
     209            return self.model._meta.default_select_fields
     210
    200211    def results_iter(self):
    201212        """
    202213        Returns an iterator over the results from executing this query.
     
    210221                        # We only set this up here because
    211222                        # related_select_fields isn't populated until
    212223                        # execute_sql() has been called.
    213                         if self.select_fields:
    214                             fields = self.select_fields + self.related_select_fields
    215                         else:
    216                             fields = self.model._meta.fields
     224                        fields = self.get_base_fields()
     225                        if self.related_select_fields:
     226                            fields = fields + self.related_select_fields
    217227                    row = self.resolve_columns(row, fields)
    218228                yield row
    219229
     
    422432
    423433        If 'with_aliases' is true, any column names that are duplicated
    424434        (without the table names) are given unique aliases. This is needed in
    425         some cases to avoid ambiguitity with nested queries.
     435        some cases to avoid ambiguitity with nested queries.       
    426436        """
    427437        qn = self.quote_name_unless_alias
    428438        qn2 = self.connection.ops.quote_name
     
    432442            col_aliases = aliases.copy()
    433443        else:
    434444            col_aliases = set()
     445           
    435446        if self.select:
    436447            for col in self.select:
    437448                if isinstance(col, (list, tuple)):
    438                     r = '%s.%s' % (qn(col[0]), qn(col[1]))
    439                     if with_aliases and col[1] in col_aliases:
    440                         c_alias = 'Col%d' % len(col_aliases)
    441                         result.append('%s AS %s' % (r, c_alias))
    442                         aliases.add(c_alias)
    443                         col_aliases.add(c_alias)
    444                     else:
    445                         result.append(r)
    446                         aliases.add(r)
    447                         col_aliases.add(col[1])
     449                    result.append(self._get_column(                           
     450                        table_alias=col[0],field=col[1],
     451                        with_aliases=with_aliases,
     452                        col_aliases=col_aliases,
     453                        aliases=aliases))
    448454                else:
    449455                    result.append(col.as_sql(quote_func=qn))
    450456                    if hasattr(col, 'alias'):
    451457                        aliases.add(col.alias)
    452458                        col_aliases.add(col.alias)
    453         elif self.default_cols:
    454             cols, new_aliases = self.get_default_columns(with_aliases,
    455                     col_aliases)
    456             result.extend(cols)
    457             aliases.update(new_aliases)
    458         for table, col in self.related_select_cols:
    459             r = '%s.%s' % (qn(table), qn(col))
    460             if with_aliases and col in col_aliases:
    461                 c_alias = 'Col%d' % len(col_aliases)
    462                 result.append('%s AS %s' % (r, c_alias))
    463                 aliases.add(c_alias)
    464                 col_aliases.add(c_alias)
    465             else:
    466                 result.append(r)
    467                 aliases.add(r)
    468                 col_aliases.add(col)
     459        else:
     460            result.extend(self.get_base_columns(with_aliases, col_aliases, aliases))
     461       
     462        for table, field in self.related_select_cols:
     463            result.append(self._get_column(table, field,
     464                with_aliases, col_aliases, aliases))
    469465
    470466        self._select_aliases = aliases
    471467        return result
    472468
    473     def get_default_columns(self, with_aliases=False, col_aliases=None,
    474             start_alias=None, opts=None, as_pairs=False):
     469    def _get_table_alias(self, model, seen, root_pk):
    475470        """
    476         Computes the default columns for selecting every field in the base
    477         model.
     471        Gets a table alias for the given model. Seen is a dict
     472        mapping already-used models to table aliases, and may be updated
     473        as a side effect.
     474        """   
     475        try:
     476            return seen[model]
     477        except KeyError:           
     478            alias = self.join((seen[None], model._meta.db_table,
     479                    root_pk, model._meta.pk.column))
     480            seen[model] = alias       
     481            return alias
     482           
     483    def get_base_columns(self, with_aliases, col_aliases, aliases):
     484        """
     485        Computes the columns to select on the base model. By default, this includes all
     486        non-lazy fields. However, this list can be modified by calling toggle_fields.
    478487
    479         Returns a list of strings, quoted appropriately for use in SQL
    480         directly, as well as a set of aliases used in the select statement (if
    481         'as_pairs' is True, returns a list of (alias, col_name) pairs instead
    482         of strings as the first component and None as the second component).
    483         """
    484         result = []
    485         if opts is None:
    486             opts = self.model._meta
    487         if start_alias:
    488             table_alias = start_alias
    489         else:
    490             table_alias = self.tables[0]
     488        Returns a list of strings, quoted appropriately for use in SQL directly.
     489        As a side effect, updates the aliases and col_aliases sets passed in as an argument.
     490        """     
     491        result = []       
     492        opts = self.model._meta
    491493        root_pk = opts.pk.column
    492         seen = {None: table_alias}
    493         qn = self.quote_name_unless_alias
    494         qn2 = self.connection.ops.quote_name
     494        seen = {None: self.tables[0]}
    495495        aliases = set()
    496         for field, model in opts.get_fields_with_model():
    497             try:
    498                 alias = seen[model]
    499             except KeyError:
    500                 alias = self.join((table_alias, model._meta.db_table,
    501                         root_pk, model._meta.pk.column))
    502                 seen[model] = alias
    503             if as_pairs:
    504                 result.append((alias, field.column))
    505                 continue
    506             if with_aliases and field.column in col_aliases:
    507                 c_alias = 'Col%d' % len(col_aliases)
    508                 result.append('%s.%s AS %s' % (qn(alias),
    509                     qn2(field.column), c_alias))
    510                 col_aliases.add(c_alias)
    511                 aliases.add(c_alias)
    512             else:
    513                 r = '%s.%s' % (qn(alias), qn2(field.column))
    514                 result.append(r)
    515                 aliases.add(r)
    516                 if with_aliases:
    517                     col_aliases.add(field.column)
    518         if as_pairs:
    519             return result, None
    520         return result, aliases
     496       
     497        if self.custom_fields:
     498            fields_with_model = [(f, opts.get_field_by_name(f.name)[1]) for f in self.select_fields]
     499        else:   
     500            fields_with_model = opts.get_default_select_fields_with_model()
     501       
     502        for field, model in fields_with_model:
     503            table_alias = self._get_table_alias(model, seen, root_pk)   
     504            result.append(self._get_column(table_alias, field,
     505                                           with_aliases, col_aliases, aliases))
     506               
     507        return result
     508   
     509    def get_related_columns(self, start_alias, opts):
     510        """
     511        Computes the default columns for selecting every non-lazy field for a related
     512        model (described by opts). The related field columns currently cannot be
     513        customized per-query.
    521514
     515        Returns a list of (alias, field) pairs, where alias is a string quoted
     516        appropriately for use in SQL directly.
     517        """
     518        result = []
     519        seen = {None: start_alias}
     520        root_pk = opts.pk.column
     521       
     522        for field, model in opts.get_default_select_fields_with_model():
     523            result.append((self._get_table_alias(model, seen, root_pk), field))
     524               
     525        return result
     526
     527    def _get_field_select(self, table_alias, field):
     528        """
     529        Returns a string with a SQL column name, quoted appropriately for use
     530        in a SQL select statement.
     531        """   
     532        return '%s.%s' % (self.quote_name_unless_alias(table_alias),
     533                          self.connection.ops.quote_name(field.column))
     534
     535
     536    def _get_column(self, table_alias, field, with_aliases, col_aliases, aliases):       
     537        """
     538        Returns a string for a SQL select statement, containing a column name
     539        as well as a column alias if necessary.
     540       
     541        The col_aliases and aliases sets are modified as a side effect.
     542        """               
     543        r = self._get_field_select(table_alias, field)
     544       
     545        if with_aliases and column_name in col_aliases:
     546            c_alias = 'Col%d' % len(col_aliases)
     547           
     548            col_aliases.add(c_alias)
     549            aliases.add(c_alias)
     550           
     551            return '%s AS %s' % (r, c_alias)
     552        else:
     553            aliases.add(r)
     554            if with_aliases:
     555                col_aliases.add(column_name)   
     556               
     557            return r
     558
    522559    def get_from_clause(self):
    523560        """
    524561        Returns a list of strings that are joined together to go after the
     
    873910            self.tables[pos] = new_alias
    874911        self.change_aliases(change_map)
    875912
     913    def toggle_fields(self, fetch_only, lazy, fetch):
     914
     915        self.custom_fields = True           
     916
     917        opts = self.model._meta
     918
     919        if fetch_only is not None:
     920            self.select_fields = []
     921           
     922            if opts.pk.attname not in fetch_only:
     923                self.select_fields.append(opts.pk)
     924           
     925            for field_name in fetch_only:
     926                self.select_fields.append(opts.get_field(field_name))
     927
     928        if not self.select_fields:
     929            self.select_fields = opts.default_select_fields[:]           
     930       
     931        if fetch is not None:
     932            for field_name in fetch:
     933                self.select_fields.append(opts.get_field(field_name))
     934       
     935        if lazy is not None:
     936            self.select_fields = [f for f in self.select_fields if f.name not in lazy]
     937
    876938    def get_initial_alias(self):
    877939        """
    878940        Returns the first alias for this query, after increasing its reference
     
    10461108                    f.rel.get_related_field().column),
    10471109                    exclusions=used.union(avoid), promote=promote)
    10481110            used.add(alias)
    1049             self.related_select_cols.extend(self.get_default_columns(
    1050                 start_alias=alias, opts=f.rel.to._meta, as_pairs=True)[0])
    1051             self.related_select_fields.extend(f.rel.to._meta.fields)
     1111            new_related_cols = self.get_related_columns(start_alias=alias, opts=f.rel.to._meta)           
     1112            self.related_select_cols.extend(new_related_cols)
     1113            self.related_select_fields.extend(f.rel.to._meta.default_select_fields)
    10521114            if restricted:
    10531115                next = requested.get(f.name, {})
    10541116            else:
     
    12851347        dupe_set = set()
    12861348        exclusions = set()
    12871349        extra_filters = []
     1350        last_pos = len(names) - 1               
     1351       
    12881352        for pos, name in enumerate(names):
    12891353            try:
    12901354                exclusions.add(int_alias)
     
    13731437                                dupe_multis, exclusions, nullable=True,
    13741438                                reuse=can_reuse)
    13751439                        joins.extend([int_alias, alias])
    1376                 elif field.rel:
    1377                     # One-to-one or many-to-one field
     1440                if not field.rel or pos == last_pos:
     1441                    # Not a one-to-one or many-to-one field.
     1442                    #
     1443                    # Except, we do include one-to-one or many-to-one fields if their name is at the
     1444                    # end of the list.E.g., if names = ["fk_id"], we don't want to do any joins because
     1445                    # fk_id is stored in that row directly.
     1446                   
     1447                    target = field
     1448                    break               
     1449                else:
     1450                    # One-to-one or many-to-one field, except as described above.
    13781451                    if cached_data:
    13791452                        (table, from_col, to_col, opts, target) = cached_data
    13801453                    else:
     
    13891462                    alias = self.join((alias, table, from_col, to_col),
    13901463                            exclusions=exclusions, nullable=field.null)
    13911464                    joins.append(alias)
    1392                 else:
    1393                     # Non-relation fields.
    1394                     target = field
    1395                     break
    13961465            else:
    13971466                orig_field = field
    13981467                field = field.field
     
    15281597                        name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
    15291598                        True)
    15301599                final_alias = joins[-1]
    1531                 col = target.column
    1532                 if len(joins) > 1:
    1533                     join = self.alias_map[final_alias]
    1534                     if col == join[RHS_JOIN_COL]:
    1535                         self.unref_alias(final_alias)
    1536                         final_alias = join[LHS_ALIAS]
    1537                         col = join[LHS_JOIN_COL]
    1538                         joins = joins[:-1]
    15391600                self.promote_alias_chain(joins[1:])
    1540                 self.select.append((final_alias, col))
     1601                self.select.append((final_alias, target))
    15411602                self.select_fields.append(field)
    15421603        except MultiJoin:
    15431604            raise FieldError("Invalid field name: '%s'" % name)
  • django/db/models/base.py

     
    1010
    1111import django.db.models.manager     # Imported to register signal handler.
    1212from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
    13 from django.db.models.fields import AutoField
     13from django.db.models.fields import AutoField, LazyDescriptor
    1414from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
    1515from django.db.models.query import delete_objects, Q, CollectedObjects
    1616from django.db.models.options import Options
     
    2828    """
    2929    def __new__(cls, name, bases, attrs):
    3030        super_new = super(ModelBase, cls).__new__
     31
     32        #
     33        # The LazyModel subclass passes _lazy_fields in attrs. In this case we quickly
     34        # create a new Model subclass which is identical to its parent class
     35        # (sharing e.g. the same _meta object), except for its _lazy_fields set.
     36        # This allows certain fields of the Model to be lazily-loaded
     37        # when they are first used, and only saved to the database if they're changed.
     38        #
     39        lazy_fields = attrs.get('_lazy_fields', None)
     40        if lazy_fields is not None:
     41            return super_new(cls, bases[0].__name__, bases, attrs)
     42       
    3143        parents = [b for b in bases if isinstance(b, ModelBase)]
    3244        if not parents:
    3345            # If this isn't a subclass of Model, don't do anything special.
     
    7082        if getattr(new_class, '_default_manager', None):
    7183            new_class._default_manager = None
    7284
     85        # make sure subclasses don't share _lazy_fields sets
     86        new_class._lazy_fields = None
     87
    7388        # Bail out early if we have already created this class.
    7489        m = get_model(new_class._meta.app_label, name, False)
    7590        if m is not None:
     
    225240
    226241        for field in fields_iter:
    227242            rel_obj = None
     243            use_default = False
     244           
    228245            if kwargs:
    229246                if isinstance(field.rel, ManyToOneRel):
    230247                    try:
     
    235252                            # Object instance wasn't passed in -- must be an ID.
    236253                            val = kwargs.pop(field.attname)
    237254                        except KeyError:
    238                             val = field.get_default()
     255                            use_default = True
    239256                    else:
    240257                        # Object instance was passed in. Special case: You can
    241258                        # pass in "None" for related objects if it's allowed.
    242259                        if rel_obj is None and field.null:
    243260                            val = None
    244261                else:
    245                     val = kwargs.pop(field.attname, field.get_default())
     262                    try:
     263                        val = kwargs.pop(field.attname)
     264                    except KeyError:
     265                        use_default = True
    246266            else:
    247                 val = field.get_default()
     267                use_default = True
     268               
     269            if use_default:           
     270                if self._lazy_fields is not None and (field.attname in self._lazy_fields): 
     271                    continue
     272
     273                val = field.get_default()   
     274               
    248275            # If we got passed a related instance, set it using the field.name
    249276            # instead of field.attname (e.g. "user" instead of "user_id") so
    250277            # that the object gets properly cached (and type checked) by the
     
    264291            if kwargs:
    265292                raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0]
    266293        signals.post_init.send(sender=self.__class__, instance=self)
     294   
     295    @classmethod
     296    def _new_from_select_values(cls, vals, fields):   
     297        """
     298        Creates a new instance of this model class.
     299        The class will be instantiated as if calling __init__ with kwargs
     300        fields[i]=vals[i] for all i (i.e., fields and vals are parallel lists).
     301       
     302        However, it may call __init__ using positional arguments when all
     303        fields are provided (i.e., there are no lazy fields).         
     304        """   
     305   
     306        # we use pointer-equality to make the test faster in the normal case   
     307        all_fields = fields is cls._meta.fields
     308   
     309        if all_fields:
     310            obj = cls(*vals) # the comments for __init__ indicate this is faster
     311        else:           
     312            obj = cls(**dict(zip([f.attname for f in fields], vals)))                 
    267313
     314        #
     315        # We may have set a default-lazy field in the constructor; if so, we
     316        # clear its dirty bit since that value came straight from a SELECT.
     317        # However, we optimize the case where there are no lazy fields, to avoid
     318        # making object instantiation slower in the normal case.
     319        #
     320        if cls._lazy_fields is not None:
     321            for field in cls._meta.fields:
     322                field.mark_unchanged(obj)               
     323                       
     324        return obj                   
     325
     326    @classmethod
     327    def _get_lazy_subclass(cls, non_lazy_fields):
     328        """
     329        Returns a subclass which is identical to the given class, except that
     330        every field not in non_lazy_fields becomes lazy. However, any field
     331        that was already lazy in the given class stays lazy in the subclass.
     332        """       
     333        if non_lazy_fields is cls._meta.fields:
     334            return cls
     335   
     336        non_lazy_names = set()
     337        for field in non_lazy_fields:
     338            non_lazy_names.add(field.name)
     339           
     340        new_lazy_fields = [f for f in cls._meta.fields if f.name not in non_lazy_names
     341                                                       and not cls.is_lazy_field(f)]
     342       
     343        if new_lazy_fields:   
     344                       
     345            # Any field which is lazy in the base class remains lazy in the
     346            # subclass, even if it's in non_lazy_fields. The reasoning for this
     347            # is that default-lazy fields likely have a lot of data, so we
     348            # probably still want to check if it's dirty when we call save().
     349
     350            lazy_fields = set([f.attname for f in new_lazy_fields])
     351            if cls._lazy_fields is not None:
     352                lazy_fields.update(cls._lazy_fields)
     353                   
     354            class LazyModel(cls):               
     355                _lazy_fields = lazy_fields
     356
     357            for field in new_lazy_fields:   
     358                setattr(LazyModel, field.attname, LazyDescriptor(field))
     359           
     360            return LazyModel
     361        else:
     362            return cls
     363
     364    @classmethod
     365    def is_lazy_field(cls, field):
     366        return cls._lazy_fields is not None and (field.attname in cls._lazy_fields)
     367
    268368    def __repr__(self):
    269369        return smart_str(u'<%s: %s>' % (self.__class__.__name__, unicode(self)))
    270370
     
    341441                self.save_base(raw, parent)
    342442                setattr(self, field.attname, self._get_pk_val(parent._meta))
    343443
    344         non_pks = [f for f in meta.local_fields if not f.primary_key]
     444        non_pks = [f for f in meta.local_fields if (not f.primary_key) and f.write_on_save(self)]
    345445
    346446        # First, try an UPDATE. If that doesn't update anything, do an INSERT.
    347447        pk_val = self._get_pk_val(meta)
     
    364464            if not pk_set:
    365465                if force_update:
    366466                    raise ValueError("Cannot force an update in save() with no primary key.")
    367                 values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)]
     467                values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField) and f.write_on_save(self)]
    368468            else:
    369                 values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields]
     469                values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if f.write_on_save(self)]
    370470
    371471            if meta.order_with_respect_to:
    372472                field = meta.order_with_respect_to
     
    384484            if update_pk:
    385485                setattr(self, meta.pk.attname, result)
    386486        transaction.commit_unless_managed()
     487       
     488        for f in non_pks:
     489            f.mark_unchanged(self)
    387490
    388491        if signal:
    389492            signals.post_save.send(sender=self.__class__, instance=self,
     
    405508            return
    406509
    407510        for related in self._meta.get_all_related_objects():
     511            if related.opts.is_view:
     512                continue
     513           
    408514            rel_opts_name = related.get_accessor_name()
    409515            if isinstance(related.field.rel, OneToOneRel):
    410516                try:
  • django/db/models/manager.py

     
    134134    def reverse(self, *args, **kwargs):
    135135        return self.get_query_set().reverse(*args, **kwargs)
    136136
     137    def toggle_fields(self, *args, **kwargs):
     138        return self.get_query_set().toggle_fields(*args, **kwargs)
     139
    137140    def _insert(self, values, **kwargs):
    138141        return insert_query(self.model, values, **kwargs)
    139142
  • django/db/models/options.py

     
    4242        self.pk = None
    4343        self.has_auto_field, self.auto_field = False, None
    4444        self.one_to_one_field = None
    45         self.abstract = False
     45        self.has_lazy_field = False         
     46        self.abstract = False       
    4647        self.parents = SortedDict()
    4748        self.duplicate_targets = {}
    4849        # Managers that have been inherited from abstract base classes. These
     
    156157        if hasattr(self, '_name_map'):
    157158            del self._name_map
    158159
     160        if field.lazy:
     161            self.has_lazy_field = True                       
     162
    159163    def add_virtual_field(self, field):
    160164        self.virtual_fields.append(field)
    161165
     
    198202        return self._field_name_cache
    199203    fields = property(_fields)
    200204
     205    def _default_select_fields(self):
     206        try:
     207            return self._default_field_name_cache
     208        except AttributeError:   
     209            self._fill_fields_cache()
     210            return self._default_field_name_cache   
     211           
     212    default_select_fields = property(_default_select_fields)           
     213           
     214    def get_default_select_fields_with_model(self):
     215        """
     216        Returns a sequence of (field, model) pairs for only those fields which
     217        are fetched by default on a SELECT query. The "model"
     218        element is None for fields on the current model. Mostly of use when
     219        constructing queries so that we know which model a field belongs to.
     220        """
     221        try:
     222            self._default_field_cache
     223        except AttributeError:
     224            self._fill_fields_cache()
     225        return self._default_field_cache
     226
    201227    def get_fields_with_model(self):
    202228        """
    203229        Returns a sequence of (field, model) pairs for all fields. The "model"
     
    221247        cache.extend([(f, None) for f in self.local_fields])
    222248        self._field_cache = tuple(cache)
    223249        self._field_name_cache = [x for x, _ in cache]
     250       
     251        if self.has_lazy_field:
     252            self._default_field_cache = tuple([(field, model) for field, model in cache if not field.lazy])
     253            self._default_field_name_cache = [x for x, _ in self._default_field_cache]
     254        else:   
     255            self._default_field_cache = self._field_cache
     256            self._default_field_name_cache = self._field_name_cache
     257       
    224258
    225259    def _many_to_many(self):
    226260        try:
  • django/db/models/fields/__init__.py

     
    6464            db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
    6565            serialize=True, unique_for_date=None, unique_for_month=None,
    6666            unique_for_year=None, choices=None, help_text='', db_column=None,
    67             db_tablespace=None, auto_created=False):
     67            db_tablespace=None, auto_created=False, lazy=False):
    6868        self.name = name
    6969        self.verbose_name = verbose_name
    7070        self.primary_key = primary_key
     
    8585        self.db_column = db_column
    8686        self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE
    8787        self.auto_created = auto_created
     88        self.lazy = lazy
    8889
    8990        # Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
    9091        self.db_index = db_index
     
    9798            self.creation_counter = Field.creation_counter
    9899            Field.creation_counter += 1
    99100
     101    def write_on_save(self, instance):
     102        return not instance.is_lazy_field(self) or getattr(instance, self.get_changed_name(), False)
     103   
     104    def mark_unchanged(self, instance):
     105        if instance.is_lazy_field(self):
     106            setattr(instance, self.get_changed_name(), False)             
     107
    100108    def __cmp__(self, other):
    101109        # This is needed because bisect does not take a comparison function.
    102110        return cmp(self.creation_counter, other.creation_counter)
     
    160168        if self.choices:
    161169            setattr(cls, 'get_%s_display' % self.name, curry(cls._get_FIELD_display, field=self))
    162170
     171        if self.lazy:             
     172            if cls._lazy_fields is None:
     173                cls._lazy_fields = set()           
     174            cls._lazy_fields.add(self.attname)
     175            setattr(cls, self.attname, LazyDescriptor(self))
     176           
    163177    def get_attname(self):
    164178        return self.name
    165179
     
    171185    def get_cache_name(self):
    172186        return '_%s_cache' % self.name
    173187
     188    def get_changed_name(self):
     189        return '_%s_changed' % self.attname   
     190
    174191    def get_internal_type(self):
    175192        return self.__class__.__name__
    176193
     
    870887    def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
    871888        self.schema_path = schema_path
    872889        Field.__init__(self, verbose_name, name, **kwargs)
     890
     891class LazyDescriptor(object): 
     892    """
     893    A descriptor for a field that is lazy-loaded from the database the first time it is accessed.
     894    Useful for large fields that are infrequently accessed by Django.
     895    """
     896
     897    def __init__(self, lazy_field):
     898        self.field = lazy_field
     899       
     900        # we use a different cache_name than field.get_cache_name() to allow lazy foreign key fields
     901        self.cache_name = '_%s_lcache' % lazy_field.attname         
     902       
     903        if self.field.primary_key:
     904            raise TypeError, "A model's primary key field cannot be lazy."
     905 
     906    def __get__(self, instance, instance_type=None):
     907        try:                 
     908            return getattr(instance, self.cache_name)
     909        except AttributeError:           
     910         
     911            pkVal = instance._get_pk_val()
     912            if pkVal is None:
     913                val = self.field.get_default()
     914            else:
     915                results = instance.__class__._default_manager.filter(pk=pkVal).values(self.field.attname)                 
     916                val = results[0][self.field.attname] # could be error if deleted
     917 
     918            setattr(instance, self.cache_name, val)
     919            return val
     920 
     921    def __set__(self, instance, value):     
     922        setattr(instance, self.cache_name, value)
     923        setattr(instance, self.field.get_changed_name(), True)
  • django/db/models/query.py

     
    272272        max_depth = self.query.max_depth
    273273        extra_select = self.query.extra_select.keys()
    274274        index_start = len(extra_select)
     275       
     276        fields = self.query.get_base_fields()
     277        # Get a subclass which knows which fields are lazy and which are not.
     278        # In the case where all fields are non-lazy, this is just self.model.
     279        lazy_model = self.model._get_lazy_subclass(fields)       
     280       
    275281        for row in self.query.results_iter():
    276282            if fill_cache:
    277                 obj, _ = get_cached_row(self.model, row, index_start,
    278                         max_depth, requested=requested)
     283                obj, _ = get_cached_row(lazy_model, row, index_start,
     284                        max_depth, requested=requested, fields=fields)
    279285            else:
    280                 obj = self.model(*row[index_start:])
     286                obj = lazy_model._new_from_select_values(row[index_start:], fields)
    281287            for i, k in enumerate(extra_select):
    282288                setattr(obj, k, row[i])
    283289            yield obj
    284290
     291    def toggle_fields(self, fetch_only=None, lazy=None, fetch=None):
     292        """
     293        Change the fields that are retrieved by this query.
     294        """       
     295   
     296        assert self.query.can_filter(), "Cannot change a query once a slice has been taken"
     297        clone = self._clone()
     298        clone.query.toggle_fields(fetch_only, lazy, fetch)
     299        return clone   
     300
    285301    def count(self):
    286302        """
    287303        Performs a SELECT COUNT() and returns the number of records as an
     
    679695                    else:
    680696                        field_names.append(f)
    681697        else:
    682             # Default to all fields.
    683             field_names = [f.attname for f in self.model._meta.fields]
     698            # Default to all non-lazy fields.
     699            field_names = [f.attname for f in self.model._meta.default_select_fields]
    684700
    685701        self.query.add_fields(field_names, False)
    686         self.query.default_cols = False
    687702        self.field_names = field_names
    688703
    689704    def _clone(self, klass=None, setup=False, **kwargs):
     
    788803
    789804
    790805def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0,
    791                    requested=None):
     806                   requested=None, fields=None):
    792807    """
    793808    Helper function that recursively returns an object with the specified
    794809    related attributes already populated.
     
    797812        # We've recursed deeply enough; stop now.
    798813        return None
    799814
     815    if fields is None:
     816        fields = klass._meta.default_select_fields   
     817
    800818    restricted = requested is not None
    801     index_end = index_start + len(klass._meta.fields)
    802     fields = row[index_start:index_end]
     819    index_end = index_start + len(fields)
     820    values = row[index_start:index_end]
    803821    if not [x for x in fields if x is not None]:
    804822        # If we only have a list of Nones, there was not related object.
    805823        obj = None
    806824    else:
    807         obj = klass(*fields)
     825        obj = klass._new_from_select_values(values, fields)
    808826    for f in klass._meta.fields:
    809827        if not select_related_descend(f, restricted, requested):
    810828            continue
  • django/contrib/gis/db/models/sql/query.py

     
    4040        obj.extra_select_fields = self.extra_select_fields.copy()
    4141        return obj
    4242
    43     def get_columns(self, with_aliases=False):
     43    def _get_field_select(self, table_alias, field):
    4444        """
    45         Return the list of columns to use in the select statement. If no
    46         columns have been specified, returns all columns relating to fields in
    47         the model.
    48 
    49         If 'with_aliases' is true, any column names that are duplicated
    50         (without the table names) are given unique aliases. This is needed in
    51         some cases to avoid ambiguitity with nested queries.
    52 
    53         This routine is overridden from Query to handle customized selection of
    54         geometry columns.
    55         """
    56         qn = self.quote_name_unless_alias
    57         qn2 = self.connection.ops.quote_name
    58         result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
    59                   for alias, col in self.extra_select.iteritems()]
    60         aliases = set(self.extra_select.keys())
    61         if with_aliases:
    62             col_aliases = aliases.copy()
     45        Returns the SELECT SQL string for the given field.  Figures out
     46        if any custom selection SQL is needed for the column  The `alias`
     47        keyword may be used to manually specify the database table where
     48        the column exists, if not in the model associated with this
     49        `GeoQuery`.
     50        """               
     51        if field in self.custom_select:
     52            return self.get_select_format(field) % self.custom_select[field]
    6353        else:
    64             col_aliases = set()
    65         if self.select:
    66             # This loop customized for GeoQuery.
    67             for col, field in izip(self.select, self.select_fields):
    68                 if isinstance(col, (list, tuple)):
    69                     r = self.get_field_select(field, col[0])
    70                     if with_aliases and col[1] in col_aliases:
    71                         c_alias = 'Col%d' % len(col_aliases)
    72                         result.append('%s AS %s' % (r, c_alias))
    73                         aliases.add(c_alias)
    74                         col_aliases.add(c_alias)
    75                     else:
    76                         result.append(r)
    77                         aliases.add(r)
    78                         col_aliases.add(col[1])
    79                 else:
    80                     result.append(col.as_sql(quote_func=qn))
    81                     if hasattr(col, 'alias'):
    82                         aliases.add(col.alias)
    83                         col_aliases.add(col.alias)
    84         elif self.default_cols:
    85             cols, new_aliases = self.get_default_columns(with_aliases,
    86                     col_aliases)
    87             result.extend(cols)
    88             aliases.update(new_aliases)
    89         # This loop customized for GeoQuery.
    90         if not self.aggregate:
    91             for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
    92                 r = self.get_field_select(field, table)
    93                 if with_aliases and col in col_aliases:
    94                     c_alias = 'Col%d' % len(col_aliases)
    95                     result.append('%s AS %s' % (r, c_alias))
    96                     aliases.add(c_alias)
    97                     col_aliases.add(c_alias)
    98                 else:
    99                     result.append(r)
    100                     aliases.add(r)
    101                     col_aliases.add(col)
     54            return super(GeoQuery, self)._get_field_select(table_alias, field)
    10255
    103         self._select_aliases = aliases
    104         return result
    105 
    106     def get_default_columns(self, with_aliases=False, col_aliases=None,
    107                             start_alias=None, opts=None, as_pairs=False):
    108         """
    109         Computes the default columns for selecting every field in the base
    110         model.
    111 
    112         Returns a list of strings, quoted appropriately for use in SQL
    113         directly, as well as a set of aliases used in the select statement.
    114 
    115         This routine is overridden from Query to handle customized selection of
    116         geometry columns.
    117         """
    118         result = []
    119         if opts is None:
    120             opts = self.model._meta
    121         if start_alias:
    122             table_alias = start_alias
    123         else:
    124             table_alias = self.tables[0]
    125         root_pk = self.model._meta.pk.column
    126         seen = {None: table_alias}
    127         aliases = set()
    128         for field, model in opts.get_fields_with_model():
    129             try:
    130                 alias = seen[model]
    131             except KeyError:
    132                 alias = self.join((table_alias, model._meta.db_table,
    133                         root_pk, model._meta.pk.column))
    134                 seen[model] = alias
    135             if as_pairs:
    136                 result.append((alias, field.column))
    137                 continue
    138             # This part of the function is customized for GeoQuery. We
    139             # see if there was any custom selection specified in the
    140             # dictionary, and set up the selection format appropriately.
    141             field_sel = self.get_field_select(field, alias)
    142             if with_aliases and field.column in col_aliases:
    143                 c_alias = 'Col%d' % len(col_aliases)
    144                 result.append('%s AS %s' % (field_sel, c_alias))
    145                 col_aliases.add(c_alias)
    146                 aliases.add(c_alias)
    147             else:
    148                 r = field_sel
    149                 result.append(r)
    150                 aliases.add(r)
    151                 if with_aliases:
    152                     col_aliases.add(field.column)
    153         if as_pairs:
    154             return result, None
    155         return result, aliases
    156 
    15756    def get_ordering(self):
    15857        """
    15958        This routine is overridden to disable ordering for aggregate
     
    209108            sel_fmt = sel_fmt % self.custom_select[alias]
    210109        return sel_fmt
    211110
    212     def get_field_select(self, fld, alias=None):
    213         """
    214         Returns the SELECT SQL string for the given field.  Figures out
    215         if any custom selection SQL is needed for the column  The `alias`
    216         keyword may be used to manually specify the database table where
    217         the column exists, if not in the model associated with this
    218         `GeoQuery`.
    219         """
    220         sel_fmt = self.get_select_format(fld)
    221         if fld in self.custom_select:
    222             field_sel = sel_fmt % self.custom_select[fld]
    223         else:
    224             field_sel = sel_fmt % self._field_column(fld, alias)
    225         return field_sel
    226 
    227111    def get_select_format(self, fld):
    228112        """
    229113        Returns the selection format string, depending on the requirements
  • tests/regressiontests/lazy_fields/models.py

     
     1from django.db import models
     2
     3class LazyObject3(models.Model):
     4    lazy = models.IntegerField(lazy=True)
     5    normal = models.IntegerField()
     6   
     7class LazyObject2(models.Model):
     8    normal = models.IntegerField()
     9    fk3 = models.ForeignKey(LazyObject3)
     10
     11class LazyObject(models.Model):
     12    normal = models.IntegerField()
     13    lazy = models.IntegerField(lazy=True)
     14    fk3 = models.ForeignKey(LazyObject3)
     15    fk2 = models.ForeignKey(LazyObject2)
     16    lazy2 = models.IntegerField(lazy=True, default=42)
     17       
     18def checkUnchanged(obj, fieldName):
     19    field = obj._meta.get_field(fieldName)
     20    assert not field.write_on_save(obj)       
     21
     22def checkChanged(obj, fieldName):
     23    field = obj._meta.get_field(fieldName)
     24    assert field.write_on_save(obj)                   
     25
     26def checkLazyField(obj, fieldName, value, cached=False):
     27    if not cached:
     28        assert not hasattr(obj,'_%s_lcache' % fieldName)
     29    else:   
     30        assert getattr(obj, '_%s_lcache' % fieldName) == value
     31
     32    assert getattr(obj, fieldName) == value
     33    assert getattr(obj, '_%s_lcache' % fieldName) == value
     34
     35def check3(_o3, correct3, eager=[]):
     36    checkLazyField(_o3, 'lazy', correct3.lazy, cached=('lazy' in eager))       
     37   
     38    assert _o3.normal == correct3.normal
     39
     40    checkUnchanged(_o3, 'lazy')
     41    _o3.lazy = 11
     42    checkChanged(_o3, 'lazy')
     43    checkLazyField(_o3, 'lazy', 11, cached=True)
     44
     45def check2(_o2, correct2, relatedDepth=0):
     46
     47    if relatedDepth > 0:
     48        assert hasattr(_o2, '_fk3_cache')           
     49    else:   
     50        assert not hasattr(_o2, '_fk3_cache')           
     51
     52    check3(_o2.fk3, correct2.fk3)
     53    assert hasattr(_o2, '_fk3_cache')           
     54
     55    assert _o2.normal == correct2.normal
     56
     57def check1(_o1, correct1, eager=[], relatedDepth=0):
     58
     59    if relatedDepth > 0:
     60        assert hasattr(_o1, '_fk2_cache')           
     61        assert hasattr(_o1, '_fk3_cache')           
     62        if relatedDepth > 1:
     63            assert hasattr(_o1._fk2_cache,'_fk3_cache')           
     64        else:   
     65            assert not hasattr(_o1._fk2_cache,'_fk3_cache')           
     66    else:
     67        assert not hasattr(_o1, '_fk2_cache')   
     68        assert not hasattr(_o1, '_fk3_cache')   
     69
     70    check2(_o1.fk2, correct1.fk2, relatedDepth=relatedDepth - 1)
     71    assert hasattr(_o1, '_fk2_cache')       
     72
     73    check3(_o1.fk3, correct1.fk3)
     74    assert hasattr(_o1, '_fk3_cache')
     75
     76    assert _o1.normal == correct1.normal
     77
     78    checkLazyField(_o1, 'lazy', correct1.lazy, cached=('lazy' in eager))
     79    checkUnchanged(_o1, 'lazy')
     80    checkUnchanged(_o1, 'lazy2')
     81    checkLazyField(_o1, 'lazy2', correct1.lazy2, cached=('lazy2' in eager))
     82
     83def checkAll(correct1, correct2, correct3):   
     84    "Test operation of lazy fields using toggle_fields and/or select_related"           
     85       
     86    check3(LazyObject3.objects.get(lazy=correct3.lazy), correct3)   
     87   
     88    check3(LazyObject3.objects.toggle_fields(fetch=['lazy']).get(lazy=correct3.lazy), correct3, eager=['lazy'])   
     89
     90    check3(LazyObject3.objects.get(normal=correct3.normal), correct3)
     91   
     92    check2(LazyObject2.objects.get(normal=correct2.normal), correct2)
     93       
     94    check2(LazyObject2.objects.select_related().get(normal=correct2.normal), correct2, relatedDepth=1)
     95   
     96    check1(LazyObject.objects.get(normal=correct1.normal), correct1)
     97   
     98    check1(LazyObject.objects.toggle_fields(fetch=['lazy']).get(normal=correct1.normal), correct1, eager=['lazy'])
     99   
     100    check1(LazyObject.objects.toggle_fields(fetch=['lazy','lazy2']).get(normal=correct1.normal), correct1, eager=['lazy','lazy2'])
     101   
     102    check1(LazyObject.objects.select_related(depth=1).get(normal=correct1.normal), correct1, relatedDepth=1)
     103   
     104    check1(LazyObject.objects.select_related(depth=2).get(normal=correct1.normal), correct1, relatedDepth=2)
     105
     106    check1(LazyObject.objects.toggle_fields(fetch=['lazy']).select_related(depth=1).get(normal=correct1.normal), correct1, eager=['lazy'], relatedDepth=1)   
     107
     108def test():
     109
     110    LazyObject.objects.all().delete()
     111    LazyObject2.objects.all().delete()
     112    LazyObject3.objects.all().delete()   
     113
     114    # desynchronize auto-increment ids of the 3 object types
     115    LazyObject3.objects.create(lazy=999,normal=999)
     116    o3_x = LazyObject3.objects.create(lazy=999,normal=999)
     117    LazyObject2.objects.create(normal=999,fk3=o3_x)
     118
     119    class orig3:
     120        lazy=1
     121        normal=2
     122   
     123    class orig2:
     124        normal=3
     125        fk3=orig3
     126   
     127    class orig1:
     128        normal=4
     129        lazy=5
     130        lazy2=6
     131        fk2=orig2
     132        fk3=orig3
     133
     134    # create the test objects
     135    o3 = LazyObject3.objects.create(lazy=orig3.lazy,normal=orig3.normal)
     136    o2 = LazyObject2.objects.create(normal=orig2.normal,fk3 =o3)
     137    o1 = LazyObject.objects.create(normal=orig1.normal,lazy=orig1.lazy,fk3=o3,fk2=o2,lazy2=orig1.lazy2)
     138       
     139    # test default values of lazy fields
     140    empty_o1 = LazyObject()
     141   
     142    checkLazyField(empty_o1, 'lazy', None, cached=False)
     143    checkLazyField(empty_o1, 'lazy2', 42, cached=False)
     144
     145    # test toggle_fields on an object with existing default-lazy fields
     146    lazy_o1 = LazyObject.objects.toggle_fields(lazy=['normal'], fetch=['lazy2']).get(lazy=orig1.lazy)
     147   
     148    checkLazyField(lazy_o1, 'normal', orig1.normal, cached=False)
     149    checkLazyField(lazy_o1, 'lazy', orig1.lazy, cached=False)
     150    checkLazyField(lazy_o1, 'lazy2', orig1.lazy2, cached=True)
     151   
     152    # test toggle_fields on an object that doesn't have any default-lazy fields
     153    lazy_o2 = LazyObject2.objects.toggle_fields(lazy=['normal']).get(fk3__lazy=orig2.fk3.lazy)
     154    checkLazyField(lazy_o2, 'normal', orig2.normal, cached=False)   
     155   
     156    # test toggle_fields with fetch_only, and lazying a foreign key
     157    lazy_o1 = LazyObject.objects.toggle_fields(fetch_only=['lazy','fk3']).get(lazy=orig1.lazy)
     158    checkLazyField(lazy_o1, 'normal', orig1.normal, cached=False)
     159    checkLazyField(lazy_o1, 'lazy', orig1.lazy, cached=True)
     160    checkLazyField(lazy_o1, 'lazy2', orig1.lazy2, cached=False)   
     161
     162    checkLazyField(lazy_o1, 'fk2_id', o2.id, cached=False)           
     163
     164    assert lazy_o1.fk2.normal == orig2.normal
     165    assert lazy_o1.fk2_id == o2.id # make sure lazy fk2_id and fk2 cache don't overwrite each other.
     166   
     167    assert lazy_o1.fk3_id == o3.id
     168   
     169    # test saving lazy fields
     170   
     171    checkAll(orig1, orig2, orig3)
     172       
     173    orig3_a = orig3()
     174    orig3_a.normal = 9
     175    o3.normal = 9   
     176    o3.save()
     177
     178    orig2_a = orig2()
     179    orig2_a.normal = 8
     180    orig2_a.fk3 = orig3_a
     181    o2.normal = 8     
     182    o2.save()
     183   
     184    orig1_a = orig1()
     185    orig1_a.lazy = 7
     186    orig1_a.fk3 = orig3_a
     187    orig1_a.fk2 = orig2_a
     188    o1.lazy = 7
     189    o1.save()
     190       
     191    checkAll(orig1_a, orig2_a, orig3_a)
     192
     193    orig1_b = orig1_a
     194    orig1_b.lazy = 10
     195    o1.lazy = 10
     196    orig1_b.lazy2 = 11
     197    o1.lazy2 = 11
     198    o1.save()
     199   
     200    checkAll(orig1_b, orig2_a, orig3_a)
     201    return "OK"
     202
     203__test__ = {'API_TESTS':"""
     204>>> test()
     205"OK"
     206"""}
Back to Top