Code

Opened 3 years ago

Last modified 12 months ago

#16281 new Bug

ContentType.get_object_for_this_type using wrong database for creating object

Reported by: tfrydrychewicz@… Owned by: poirier
Component: contrib.contenttypes Version: master
Severity: Normal Keywords: contenttype, object get_object_for_this_type, database, multiple
Cc: gklein@… Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

There is a subtle error in ContentType.get_object_for_this_type method.

def get_object_for_this_type(self, **kwargs):
    return self.model_class()._default_manager.using(self._state.db).get(**kwargs)

Database used to get model_class object is taken from self._state.db, which provides an error when contenttype model is hold in one database and model, of which object we're going to create, in another one.

Database should be provided using self.model_class().objects.db not self._state.db.

Attachments (1)

16281.diff (656 bytes) - added by tfrydrychewicz@… 3 years ago.
Patch

Download all attachments as: .zip

Change History (13)

Changed 3 years ago by tfrydrychewicz@…

Patch

comment:1 Changed 3 years ago by anonymous

  • Needs documentation unset
  • Needs tests set
  • Patch needs improvement unset

comment:2 Changed 3 years ago by melinath

  • Triage Stage changed from Unreviewed to Accepted
  • Version changed from 1.3 to SVN

comment:3 Changed 3 years ago by ramiro

See also #16088.

comment:4 Changed 3 years ago by poirier

  • Owner changed from nobody to poirier
  • Status changed from new to assigned

comment:5 Changed 3 years ago by poirier

  • Cc poirier added
  • Resolution set to invalid
  • Status changed from assigned to closed

After studying this, I'm not sure the current code is wrong. I'd want to see a use case in which Django clearly does the wrong thing to be convinced otherwise. Please re-open the ticket if there is one.

The proposed fix:

 -        return self.model_class()._base_manager.using(self._state.db).get(**kwargs)
 +        return self.model_class()._base_manager.using(self.model_class().objects.db).get(**kwargs)

seems to assume that there is exactly one database in which it is correct to look for instances of a given model, but in Django, it's not required that all instances of a model be stored in one database.

Applying the fix causes one of the multiple database tests, test_generic_key_separation(), to fail. This test puts pairs of related instances in each of the two test databases and ensures that querying for the related instances works, but after this change, is unable to find the related instance in the 2nd database correctly, probably because now get_object_for_this_type() is only looking in the default database.

The test does assume that ContentType objects exist in every database in which a model has instances, but that seems reasonable, given the requirement in Django that related objects be stored in the same database. ContentType objects don't actually have ForeignKeys, but logically they're still related to the models they represent.

The current code looks for the instance of the requested model in the same database in which the ContentType instance being used was found, which fits in with the "related instances in same database" requirement.

However, in working through all this, I found nothing in the Django documentation discussing how the content type framework and multiple databases interact, and perhaps this ticket demonstrates the need for more documentation on how to correctly use content types when multiple databases are involved.

comment:6 Changed 20 months ago by Gertjan Klein <gklein@…>

  • Resolution invalid deleted
  • Status changed from closed to reopened

I don't understand the patch or it's implications enough to comment on it. However, I can confirm this is a valid bug in Django 1.4. I have an application where some tables reside in a mysql database; these are pre-existing tables, not managed by Django, for which I created models using inspectdb. Everything else (auth, contenttypes, ...) resides in a sqlite database. A database router manages which model is mapped to which database. This works fine everywhere.

Recently, I decided to add a get_absolute_url method to one of the models residing in the mysql database. I now have a "show on site" link in the admin for that model. Without the patch above, clicking this link causes a DatabaseError exception, because Django attempts to access the model's table in the sqlite database.

It seems to me that this should just work. Django knows the model's table resides in the mysql database, because it is editing an instance of it.

The proposed fix above does indeed fix this problem for me: the link now properly links to my site. (I haven't tried trunk, but the relevant code appears to be unchanged from 1.4, so I expect it to behave the same.)

comment:7 Changed 20 months ago by Gertjan Klein <gklein@…>

  • Cc gklein@… added
  • Severity changed from Release blocker to Normal

comment:8 Changed 16 months ago by aaugustin

  • Status changed from reopened to new

comment:9 follow-up: Changed 15 months ago by anonymous

Based on poirier's earlier comment, would it not work to simply remove the .using(self._state.db) clause from the original code altogether? Then the DB router could do its usual job of deciding where to point the query.

comment:10 in reply to: ↑ 9 ; follow-up: Changed 15 months ago by Gertjan Klein <gklein@…>

Replying to anonymous:

would it not work to simply remove the .using(self._state.db) clause from the original code altogether?

I can confirm that works in my situation (described above in [comment 6 comment:6]).

comment:11 Changed 15 months ago by poirier

  • Cc poirier removed

comment:12 in reply to: ↑ 10 Changed 12 months ago by anonymous

Replying to Gertjan Klein <gklein@…>:

Replying to anonymous:

would it not work to simply remove the .using(self._state.db) clause from the original code altogether?

I can confirm that works in my situation (described above in [comment 6 comment:6]).

This also fixes the problem in for me. However, it also causes test_generic_key_separation (regressiontests.multiple_database.tests.QueryTestCase) to fail (just like the proposed fix by poirier above). This isn't a problem in my application, but it clearly is a regression elsewhere.

This change allows get_object_for_this_type to continue using the instance._state.db for the regression test case, but the router-provided database for cases that don't specify a database. I don't think this change is complete, as there are almost certainly more locations that need to pass using= in order to not regress.

diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index e83c83a..ee04262 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -123,7 +123,7 @@ class GenericForeignKey(object):
             if ct_id:
                 ct = self.get_content_type(id=ct_id, using=instance._state.db)
                 try:
-                    rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
+                    rel_obj = ct.get_object_for_this_type(using=instance._state.db, pk=getattr(instance, self.fk_field))
                 except ObjectDoesNotExist:
                     pass
             setattr(instance, self.cache_attr, rel_obj)
diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py
index b658655..4c4d92b 100644
--- a/django/contrib/contenttypes/models.py
+++ b/django/contrib/contenttypes/models.py
@@ -157,20 +157,23 @@ class ContentType(models.Model):
         return models.get_model(self.app_label, self.model,
                                 only_installed=False)
 
-    def get_object_for_this_type(self, **kwargs):
+    def get_object_for_this_type(self, using=None, **kwargs):
         """
         Returns an object of this type for the keyword arguments given.
         Basically, this is a proxy around this object_type's get_object() model
         method. The ObjectNotExist exception, if thrown, will not be caught,
         so code that calls this method should catch it.
         """
-        return self.model_class()._base_manager.using(self._state.db).get(**kwargs)
+        if using:
+            return self.model_class()._base_manager.using(using).get(**kwargs)
+        else:
+            return self.model_class()._base_manager.get(**kwargs)
 
     def get_all_objects_for_this_type(self, **kwargs):
         """
         Returns all objects of this type for the keyword arguments given.
         """
-        return self.model_class()._base_manager.using(self._state.db).filter(**kwargs)
+        return self.model_class()._base_manager.filter(**kwargs)
 
     def natural_key(self):
         return (self.app_label, self.model)

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as new
The owner will be changed from poirier to anonymous. Next status will be 'assigned'
as The resolution will be set. Next status will be 'closed'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.