9👍
This now works:
class ClientAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'mobile', 'get_patients')
def get_queryset(self, obj):
qs = super(ClientAdmin, self).get_queryset(obj)
return qs.prefetch_related('patient_fk')
def get_patients(self, obj):
return list(obj.patient_fk.all())
This page only needed 6 queries to display…
…compared to my original code (below) which was running a separate query to retrieve the patients for each client (100 clients per page)
from .models import Client, Patient
class ClientAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'mobile', 'patients')
def patients(self,obj):
p = Patient.objects.filter(client_id=obj.pk)
return list(p)
Here’s my understanding of how and why this works (feel free to point out any errors):
Every model has a Manager whose default name is objects allowing us to access the database records. To pull all records from a model, we us SomeModel.objects.all()
which – under the hood – is just the QuerySet returned by the get_queryset method of the Manager class.
So if we need to tweak what is returned from a Model – i.e. the QuerySet – then we need to override the method that grabs it, namely get_queryset. Our new method has same name as the method we want to override:
def get_queryset(self, obj):
Now, the above method knows nothing about how to get access to the modes data. It contains no code. To get access to the data we need to call the ‘real’ get_queryset method (the one we’re overriding) so that we can actually get data back, tweak it (add some extra patient info), then return it.
To access the ‘original’ get_queryset method and get a QuerySet object (containing all Model data, no patients) then we use super()
.
super()
gives us access to a method on a parent class.
For example:
In our case it lets us grab ClientAdmin’s get_queryset()
method.
def get_queryset(self, obj):
qs = super(ClientAdmin, self).get_queryset(obj)
qs
hold all the data in the Model in a QuerySet object.
To ‘add in’ all of the Patients objects that lie at the end of the one-to-many relationship (a Client can have many Patients) we use prefetch_related()
:
return qs.prefetch_related('patient_fk')'
This performs a lookup for each Client and returns any Patient objects by following the ‘patient_fk’ foreign key. This is performed under the hood by Python (not SQL) such that the end result is a new QuerySet – generated by a single database lookup – containing all of the data we need to not only list all of the objects in our main Model but also include related objets from other Models.
So, what happens if we do not override Manager.get_queryset()
method? Well, then we just get the data that is in the specific table (Clients), no info about Patients (…and 100 extra database hits):
class ClientAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'mobile', 'get_patients')
#do not override Manager.get_queryset()
#def get_queryset(self, obj):
# qs = super(ClientAdmin, self).get_queryset(obj)
# return qs.prefetch_related('patient_fk')
def get_patients(self, obj):
return list(obj.patient_fk.all())
#forces extra per-client query by following patient_fk
I hope this helps someone out there. Any errors in my explanation let me know and I’ll correct.
3👍
if it works :+1: !!
few notes however: it will execute one query for each Client, so if you display 100 clients on the admin, django will execute 100 queries
You could maybe improve it by changing the main queryset (like this) on the admin and using prefetch_related(‘patients’)
should be something like:
class ClientAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'mobile', 'patients')
def get_queryset(self, request):
qs = super(ClientAdmin, self).get_queryset(request)
return qs.prefetch_related('patients') # do read the doc, maybe 'patients' is not the correct lookup for you
def patients(self,obj):
return self.patients_set.all() # since you have prefetched the patients I think it wont hit the database, to be tested
Hope this helps
Note:
you can get all the Patients related to a Client using the related object reference, something like:
# get one client
client = Client.objects.last()
# get all the client's patient
patients = client.patient_set.all()
the last line is similar to:
patients = Patient.objects.get(client=client)
finally you can override the patient_set
name and make it prettier, read https://docs.djangoproject.com/en/1.9/topics/db/queries/#following-relationships-backward
I haven’t tested it, It would be nice to have a feedback to see if this will prevent the n+1 problem
- [Django]-In Django, how to rename user model?
- [Django]-Django 1.6 + RabbitMQ 3.2.3 + Celery 3.1.9 – why does my celery worker die with: WorkerLostError: Worker exited prematurely: signal 11 (SIGSEGV)
- [Django]-Cannot Connect to Django from outside the Local Server
- [Django]-Type object is not iterable Django
- [Django]-How to make a geography field unique?
0👍
def patients(self,obj):
p = obj.patients.all()
return list(p)
this is assuming that in your ForeignKey you set related_name='patients'
EDIT: fixed mistake
EDIT2: changed reverse_name to related_name and added ‘.all()’
- [Django]-Redirect http to https safely for Heroku app
- [Django]-Django – use same column for two foreign keys
- [Django]-Django annotate multiple objects based on multiple fields on a M2M relationship
- [Django]-How to make a geography field unique?
- [Django]-Django log errors and traceback in file in production environment