Ticket #19501: fast_init.diff

File fast_init.diff, 14.6 KB (added by Anssi Kääriäinen, 11 years ago)
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    index bf9854d..b0a5094 100644
    a b class Model(six.with_metaclass(ModelBase, object)):  
    407407        super(Model, self).__init__()
    408408        signals.post_init.send(sender=self.__class__, instance=self)
    409409
     410    @classmethod
     411    def from_db(cls, using, values, field_names, init_with_args, can_fast_init):
     412        if can_fast_init:
     413            new = Empty()
     414            new.__class__ = cls
     415            new.__dict__ = dict(zip(field_names, values))
     416            new._state = ModelState()
     417        elif init_with_args:
     418            new = cls(*values)
     419        else:
     420            new = cls(**dict(zip(field_names, values)))
     421        new._state.adding, new._state.db = False, using
     422        return new
     423
    410424    def __repr__(self):
    411425        try:
    412426            u = six.text_type(self)
  • django/db/models/options.py

    diff --git a/django/db/models/options.py b/django/db/models/options.py
    index 45b32b0..5e82c4c 100644
    a b from django.db.models.loading import get_models, app_cache_ready  
    1212from django.utils import six
    1313from django.utils.datastructures import SortedDict
    1414from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible
     15from django.utils.functional import cached_property
    1516from django.utils.translation import activate, deactivate_all, get_language, string_concat
    1617
    1718# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
    class Options(object):  
    352353        # the main example). Trim them.
    353354        return [val for val in names if not val.endswith('+')]
    354355
     356    @cached_property
     357    def attnames(self):
     358        return [f.attname for f in self.fields]
     359
    355360    def init_name_map(self):
    356361        """
    357362        Initialises the field name -> field object mapping.
    class Options(object):  
    520525                # of the chain to the ancestor is that parent
    521526                # links
    522527                return self.parents[parent] or parent_link
     528
     529    def can_fast_init(self, attnames):
     530        """
     531        Checks if it is possible to skip Model.__init__ and instead use
     532        a fast-path for the init.
     533
     534        The skip can be done if the model doesn't override `__int__`, doesn't
     535        have pre or post_init signals and doesn't have setters for the
     536        attnames (either through __setattr__ or descriptors).
     537        """
     538        from django.db.models import Model, signals
     539        has_descriptors = False
     540        for attname in attnames:
     541            if attname in self.model.__dict__ and hasattr(self.model.__dict__[attname], '__set__'):
     542                has_descriptors = True
     543                break
     544        return (
     545            self.model.__init__ == Model.__init__ and
     546            self.model.__setattr__ == object.__setattr__ and not
     547            signals.pre_init.has_listeners(self.model) and not
     548            signals.post_init.has_listeners(self.model) and not
     549            has_descriptors)
  • django/db/models/query.py

    diff --git a/django/db/models/query.py b/django/db/models/query.py
    index f56d5d2..d6b781e 100644
    a b from django.db.models.query_utils import (Q, select_related_descend,  
    1515    deferred_class_factory, InvalidQuery)
    1616from django.db.models.deletion import Collector
    1717from django.db.models import sql
    18 from django.utils.functional import partition
    1918from django.utils import six
     19from django.utils.functional import partition
    2020
    2121# Used to control how many objects are worked with at once in some cases (e.g.
    2222# when deleting objects).
    class QuerySet(object):  
    288288                else:
    289289                    init_list.append(field.attname)
    290290            model_cls = deferred_class_factory(self.model, skip)
     291        else:
     292            model_cls = self.model
     293            init_list = [f.attname for f in model_cls._meta.fields]
    291294
    292         # Cache db, model and known_related_object outside the loop
     295        # Pre-calculate & cache everything possible outside the loop.
    293296        db = self.db
    294297        model = self.model
    295298        kro_attname, kro_instance = self._known_related_object or (None, None)
    296299        compiler = self.query.get_compiler(using=db)
     300        can_fast_init = model._meta.can_fast_init(init_list)
     301        init_with_kwargs = not bool(skip)
    297302        if fill_cache:
    298303            klass_info = get_klass_info(model, max_depth=max_depth,
    299304                                        requested=requested, only_load=only_load)
    class QuerySet(object):  
    302307                obj, _ = get_cached_row(row, index_start, db, klass_info,
    303308                                        offset=len(aggregate_select))
    304309            else:
    305                 # Omit aggregates in object creation.
    306                 row_data = row[index_start:aggregate_start]
    307                 if skip:
    308                     obj = model_cls(**dict(zip(init_list, row_data)))
    309                 else:
    310                     obj = model(*row_data)
    311 
    312                 # Store the source database of the object
    313                 obj._state.db = db
    314                 # This object came from the database; it's not being added.
    315                 obj._state.adding = False
     310                obj = model_cls.from_db(db, row[index_start:aggregate_start], init_list,
     311                                        init_with_kwargs, can_fast_init)
    316312
    317313            if extra_select:
    318314                for i, k in enumerate(extra_select):
    def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,  
    14111407    else:
    14121408        pk_idx = klass._meta.pk_index()
    14131409
    1414     return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx
     1410    can_fast_init = klass._meta.can_fast_init(field_names)
     1411    return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx, can_fast_init
    14151412
    14161413
    14171414def get_cached_row(row, index_start, using,  klass_info, offset=0,
    def get_cached_row(row, index_start, using, klass_info, offset=0,  
    14371434    """
    14381435    if klass_info is None:
    14391436        return None
    1440     klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx = klass_info
     1437    klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx, can_fast_init = klass_info
    14411438
    1442 
    1443     fields = row[index_start : index_start + field_count]
     1439    fields = row[index_start: index_start + field_count]
    14441440    # If the pk column is None (or the Oracle equivalent ''), then the related
    14451441    # object must be non-existent - set the relation to None.
    1446     if fields[pk_idx] == None or fields[pk_idx] == '':
     1442    if fields[pk_idx] is None or fields[pk_idx] == '':
    14471443        obj = None
    14481444    elif field_names:
    14491445        fields = list(fields)
    14501446        for rel_field, value in parent_data:
    14511447            field_names.append(rel_field.attname)
    14521448            fields.append(value)
    1453         obj = klass(**dict(zip(field_names, fields)))
     1449        obj = klass.from_db(using, fields, field_names, False, can_fast_init)
    14541450    else:
    1455         obj = klass(*fields)
    1456     # If an object was retrieved, set the database state.
    1457     if obj:
    1458         obj._state.db = using
    1459         obj._state.adding = False
     1451        obj = klass.from_db(using, fields, klass._meta.attnames, True, can_fast_init)
    14601452
    14611453    # Instantiate related fields
    14621454    index_end = index_start + field_count + offset
    class RawQuerySet(object):  
    15291521        self.translations = translations or {}
    15301522
    15311523    def __iter__(self):
    1532         # Mapping of attrnames to row column positions. Used for constructing
    1533         # the model using kwargs, needed when not all model's fields are present
    1534         # in the query.
    1535         model_init_field_names = {}
    15361524        # A list of tuples of (column name, column position). Used for
    15371525        # annotation fields.
    15381526        annotation_fields = []
    class RawQuerySet(object):  
    15481536
    15491537        # Find out which columns are model's fields, and which ones should be
    15501538        # annotated to the model.
     1539        attname_to_colpos = {}
    15511540        for pos, column in enumerate(self.columns):
    15521541            if column in self.model_fields:
    1553                 model_init_field_names[self.model_fields[column].attname] = pos
     1542                attname_to_colpos[self.model_fields[column].attname] = pos
    15541543            else:
    15551544                annotation_fields.append((column, pos))
    15561545
    15571546        # Find out which model's fields are not present in the query.
    15581547        skip = set()
     1548        # List of positions of model's fetched fields in the output.
     1549        field_col_pos = []
     1550        model_init_attnames = []
    15591551        for field in self.model._meta.fields:
    1560             if field.attname not in model_init_field_names:
     1552            if field.attname not in attname_to_colpos:
    15611553                skip.add(field.attname)
     1554            else:
     1555                # Find out the pos of this field.
     1556                field_col_pos.append(attname_to_colpos[field.attname])
     1557                model_init_attnames.append(field.attname)
     1558
    15621559        if skip:
    15631560            if self.model._meta.pk.attname in skip:
    15641561                raise InvalidQuery('Raw query must include the primary key')
    15651562            model_cls = deferred_class_factory(self.model, skip)
    15661563        else:
     1564            # All model's fields are present in the query.
    15671565            model_cls = self.model
    1568             # All model's fields are present in the query. So, it is possible
    1569             # to use *args based model instantation. For each field of the model,
    1570             # record the query column position matching that field.
    1571             model_init_field_pos = []
    1572             for field in self.model._meta.fields:
    1573                 model_init_field_pos.append(model_init_field_names[field.attname])
    15741566        if need_resolv_columns:
    15751567            fields = [self.model_fields.get(c, None) for c in self.columns]
     1568        can_fast_init = model_cls._meta.can_fast_init(model_init_attnames)
     1569        init_with_args = not bool(skip)
    15761570        # Begin looping through the query values.
    15771571        for values in query:
    15781572            if need_resolv_columns:
    15791573                values = compiler.resolve_columns(values, fields)
    15801574            # Associate fields to values
    1581             if skip:
    1582                 model_init_kwargs = {}
    1583                 for attname, pos in six.iteritems(model_init_field_names):
    1584                     model_init_kwargs[attname] = values[pos]
    1585                 instance = model_cls(**model_init_kwargs)
    1586             else:
    1587                 model_init_args = [values[pos] for pos in model_init_field_pos]
    1588                 instance = model_cls(*model_init_args)
     1575            model_init_args = [values[pos] for pos in field_col_pos]
     1576            instance = model_cls.from_db(
     1577                db, model_init_args, model_init_attnames,
     1578                init_with_args, can_fast_init)
    15891579            if annotation_fields:
    15901580                for column, pos in annotation_fields:
    15911581                    setattr(instance, column, values[pos])
    15921582
    1593             instance._state.db = db
    1594             instance._state.adding = False
    1595 
    15961583            yield instance
    15971584
    15981585    def __repr__(self):
  • django/db/models/query_utils.py

    diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
    index c1a690a..a2b2814 100644
    a b def select_related_descend(field, restricted, requested, load_fields, reverse=Fa  
    166166# This function is needed because data descriptors must be defined on a class
    167167# object, not an instance, to have any effect.
    168168
     169_cls_cache = {}
    169170def deferred_class_factory(model, attrs):
    170171    """
    171172    Returns a class object that is a copy of "model" with the specified "attrs"
    172173    being replaced with DeferredAttribute objects. The "pk_value" ties the
    173174    deferred attributes to a particular instance of the model.
    174175    """
     176    cache_key = model, frozenset(attrs)
     177    if cache_key in _cls_cache:
     178        return _cls_cache[cache_key]
    175179    class Meta:
    176180        proxy = True
    177181        app_label = model._meta.app_label
    def deferred_class_factory(model, attrs):  
    188192    overrides["Meta"] = Meta
    189193    overrides["__module__"] = model.__module__
    190194    overrides["_deferred"] = True
    191     return type(str(name), (model,), overrides)
     195    ret = type(str(name), (model,), overrides)
     196    _cls_cache[cache_key] = ret
     197    return ret
    192198
    193199# The above function is also used to unpickle model instances with deferred
    194200# fields.
  • tests/regressiontests/select_related_regress/models.py

    diff --git a/tests/regressiontests/select_related_regress/models.py b/tests/regressiontests/select_related_regress/models.py
    index a291a54..0d4ebbb 100644
    a b class Item(models.Model):  
    9494
    9595    def __str__(self):
    9696        return self.name
     97
     98# Models for fast-path init tests
     99class Rel(models.Model):
     100    name = models.CharField(max_length=10)
     101    type = models.CharField(max_length=10)
     102
     103    def __init__(self, *args, **kwargs):
     104        # Override init so that fast-path is skipped
     105        super(Rel, self).__init__(*args, **kwargs)
     106
     107class Base(models.Model):
     108    name = models.CharField(max_length=10)
     109    type = models.CharField(max_length=10)
     110    rel = models.ForeignKey(Rel)
     111
     112    def __init__(self, *args, **kwargs):
     113        # Override init so that fast-path is skipped
     114        super(Base, self).__init__(*args, **kwargs)
  • tests/regressiontests/select_related_regress/tests.py

    diff --git a/tests/regressiontests/select_related_regress/tests.py b/tests/regressiontests/select_related_regress/tests.py
    index 7f93a1c..096f062 100644
    a b from django.utils import six  
    55
    66from .models import (Building, Child, Device, Port, Item, Country, Connection,
    77    ClientStatus, State, Client, SpecialClient, TUser, Person, Student,
    8     Organizer, Class, Enrollment)
     8    Organizer, Class, Enrollment, Rel, Base)
    99
    1010
    1111class SelectRelatedRegressTests(TestCase):
    class SelectRelatedRegressTests(TestCase):  
    139139        self.assertEqual(troy.name, 'Troy Buswell')
    140140        self.assertEqual(troy.value, 42)
    141141        self.assertEqual(troy.state.name, 'Western Australia')
     142
     143    def test_no_fastinit_select_related(self):
     144        r1 = Rel.objects.create(name='r1', type='r1t1')
     145        b1 = Base.objects.create(name='b1', type='b1t1', rel=r1)
     146        qs = Base.objects.select_related('rel').only('type', 'rel__type')
     147        self.assertEqual(len(qs), 1)
     148        with self.assertNumQueries(0):
     149            self.assertEqual(qs[0].type, b1.type)
     150            self.assertEqual(qs[0].rel.type, r1.type)
     151        with self.assertNumQueries(2):
     152            self.assertEqual(qs[0].name, b1.name)
     153            self.assertEqual(qs[0].rel.name, r1.name)
Back to Top