Search code examples
mysqldjangodjango-admincomposite-primary-keyinspectdb

django admin site returns MultipleObjectsReturned exception with inspectdb imported legacy database and composite primary key


Using inspectdb, I have imported a legacy database, that contains entities with composite primary keys, in django . The database schema contains about 200 different entities and inspectdb is quite handy in that situation.

This is the schema in mysql:

CREATE TABLE `mymodel` (
  `id` bigint(20) unsigned NOT NULL DEFAULT '0',
  `siteid` bigint(20) unsigned NOT NULL DEFAULT '0',
...
  PRIMARY KEY (`siteid`,`id`),
...

Following the autogenerated model in django (imported using python manager.py inspectdb)

class Mymodel(models.Model):
    id = models.PositiveBigIntegerField()
    siteid = models.PositiveBigIntegerField(primary_key=True)
...
    class Meta:
        managed = False
        db_table = 'mymodel'
        unique_together = (('siteid', 'id'),

I have registered all models in the admin site using the following approach:

from django.contrib import admin
from django.apps import apps

app = apps.get_app_config('appname')

for model_name, model in app.models.items():
    admin.site.register(model)

After all the work is done, I navigate to the admin site and click on any object in the "mymodel" section and the following exception will be returned:

appname.models.Content.MultipleObjectsReturned: get() returned more than one Mymodel-- it returned more than 20!

Obviously, (this is what it seems to me at least) admin is using the siteid to get the object, tough it should use the unique_together from the Meta class.

Any suggestions how I can achieve to solve this with a general configuration and get the admin site module to query using the unique_together?


Solution

  • Yes you can solve this problem but you put a little more effort.

    First you separate model-admin class for model Mymodel and customize model-admin class method:

    1. Since django admin build change url in ChangeList class, So we can create a custom Changelist class like MymodelChangelist and pass id field value as a query params. We will use id field value to getting object.

    2. Override get_object() method to use custom query for getting object from queryset

    3. Override get_changelist() method of model-admin to set your custom Changelist class

    4. Override save_model() method to save object explicitly.

    admin.py

    class MymodelChangelist(ChangeList):
        # override changelist class
    
        def url_for_result(self, result):
            id = getattr(result, 'id')
            pk = getattr(result, self.pk_attname)
            url = reverse('admin:%s_%s_change' % (self.opts.app_label,
                                                   self.opts.model_name),
                           args=(quote(pk),),
                           current_app=self.model_admin.admin_site.name)
    
            # Added `id` as query params to filter queryset to get unique object 
            url = url + "?id=" + str(id)
            return url
    
    
    @admin.register(Mymodel)
    class MymodelAdmin(admin.ModelAdmin):
        list_display = [
            'id', 'siteid', 'other_model_fields'
        ]
    
        def get_changelist(self, request, **kwargs):
            """
            Return the ChangeList class for use on the changelist page.
            """
            return MymodelChangelist
    
        def get_object(self, request, object_id, from_field=None):
            """
            Return an instance matching the field and value provided, the primary
            key is used if no field is provided. Return ``None`` if no match is
            found or the object_id fails validation.
            """
            queryset = self.get_queryset(request)
            model = queryset.model
            field = model._meta.pk if from_field is None else model._meta.get_field(from_field)
            try:
                object_id = field.to_python(object_id)
                # get id field value from query params
                id = request.GET.get('id')
                return queryset.get(**{'id': id, 'siteid': object_id})
            except (model.DoesNotExist, ValidationError, ValueError):
                return None
    
        def save_model(self, request, obj, form, change):
            cleaned_data = form.cleaned_data
            if change:
                id = cleaned_data.get('id')
                siteid = cleaned_data.get('siteid')
                other_fields = cleaned_data.get('other_fields')
                self.model.objects.filter(id=id, siteid=siteid).update(other_fields=other_fields)
            else:
                obj.save()
    

    Now you can update any objects and also add new object. But, On addition one case you can't add- siteid which is already added because of primary key validation