Cara tercepat untuk mendapatkan objek pertama dari queryset di Django?

193

Seringkali saya mendapati diri saya ingin mendapatkan objek pertama dari queryset di Django, atau kembali Nonejika tidak ada. Ada banyak cara untuk melakukan ini yang semuanya berfungsi. Tapi saya bertanya-tanya mana yang paling performan.

qs = MyModel.objects.filter(blah = blah)
if qs.count() > 0:
    return qs[0]
else:
    return None

Apakah ini menghasilkan dua panggilan basis data? Itu sepertinya boros. Apakah ini lebih cepat?

qs = MyModel.objects.filter(blah = blah)
if len(qs) > 0:
    return qs[0]
else:
    return None

Pilihan lain adalah:

qs = MyModel.objects.filter(blah = blah)
try:
    return qs[0]
except IndexError:
    return None

Ini menghasilkan panggilan database tunggal, yang bagus. Tetapi membutuhkan membuat objek pengecualian banyak waktu, yang merupakan hal yang sangat intensif memori untuk dilakukan ketika semua yang Anda butuhkan adalah tes-sepele sepele.

Bagaimana saya bisa melakukan ini hanya dengan satu panggilan basis data dan tanpa mengaduk memori dengan objek pengecualian?

Leopd
sumber
21
Rule of thumb: Jika Anda khawatir tentang meminimalkan round-trip DB, jangan gunakan len()pada queryset, selalu gunakan .count().
Daniel DiPaolo
7
"Membuat objek pengecualian banyak waktu, yang merupakan hal yang sangat intensif memori" - jika Anda khawatir membuat satu pengecualian tambahan, maka Anda salah melakukannya karena Python menggunakan pengecualian di semua tempat. Apakah Anda benar-benar membandingkannya dengan memori yang intensif dalam kasus Anda?
lqc
1
@Leopd Dan jika Anda benar-benar membuat benchmark server (atau setidaknya komentar), Anda akan tahu itu tidak lebih cepat. Sebenarnya mungkin lebih lambat, karena Anda membuat daftar tambahan hanya untuk membuangnya. Dan semua itu hanya kacang dibandingkan dengan biaya memanggil fungsi python atau menggunakan ORANG Django sejak awal! Panggilan tunggal untuk memfilter () banyak, banyak, berkali- kali lebih lambat daripada menaikkan pengecualian (yang masih akan dinaikkan, karena itulah cara kerja protokol iterator!).
lqc
1
Intuisi Anda benar bahwa perbedaan kinerja kecil, tetapi kesimpulan Anda salah. Saya memang menjalankan patokan dan jawaban yang diterima sebenarnya lebih cepat dengan margin nyata. Sosok pergi.
Leopd
11
Untuk orang-orang yang menggunakan Django 1.6, mereka akhirnya menambahkan metode first()dan last()kenyamanan: docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Jawaban:

328

Django 1.6 (dirilis November 2013) memperkenalkan metode kenyamanan first() dan last()yang menelan pengecualian yang dihasilkan dan kembali Nonejika queryset tidak mengembalikan objek.

cod3monk3y
sumber
1
itu tidak melakukan [: 1], jadi tidak secepat (kecuali jika Anda perlu mengevaluasi seluruh queryset).
janek37
13
juga, first()dan last()memberlakukan ORDER BYklausa pada kueri. Ini akan membuat hasilnya deterministik tetapi kemungkinan besar akan memperlambat permintaan.
Phil Krylov
@ janek37 tidak ada perbedaan dalam kinerja. Seperti yang ditunjukkan oleh cod3monk3y, ini adalah metode yang mudah dan tidak membaca seluruh queryset.
Zompa
142

Jawaban yang benar adalah

Entry.objects.all()[:1].get()

Yang dapat digunakan di:

Entry.objects.filter()[:1].get()

Anda tidak ingin mengubahnya menjadi daftar karena akan memaksa panggilan database penuh dari semua catatan. Lakukan saja di atas dan itu hanya akan menarik yang pertama. Anda bahkan dapat menggunakan .order_byuntuk memastikan Anda mendapatkan yang pertama Anda inginkan.

Pastikan untuk menambahkan .get()atau Anda akan mendapatkan QuerySet kembali dan bukan objek.

stormlifter
sumber
9
Anda masih perlu membungkusnya dalam percobaan ... kecuali ObjectDoesNotExist, yang seperti opsi ketiga asli tetapi dengan mengiris.
Danny W. Adair
1
Apa gunanya menetapkan LIMIT jika Anda akan mendapatkan panggilan () pada akhirnya? Biarkan ORM dan kompiler SQL memutuskan apa yang terbaik untuk backend itu (misalnya, pada Oracle Django mengemulasi LIMIT, jadi itu akan menyakitkan daripada membantu).
lqc
Saya menggunakan jawaban ini tanpa .get (). Jika daftar dikembalikan saya kemudian mengembalikan elemen pertama dari daftar.
Keith John Hutchison
apa yang berbeda dari memiliki Entry.objects.all()[0]??
James Lin
15
@ JamesLin Perbedaannya adalah [: 1] .get () memunculkan DoesNotExist, sementara [0] memunculkan IndexError.
Ropez
49
r = list(qs[:1])
if r:
  return r[0]
return None
Ignacio Vazquez-Abrams
sumber
1
Jika Anda mengaktifkan penelusuran, saya cukup yakin Anda bahkan akan melihat ini menambah LIMIT 1kueri, dan saya tidak tahu bahwa Anda bisa melakukan yang lebih baik dari ini. Namun, secara internal __nonzero__di QuerySetdiimplementasikan sebagai try: iter(self).next() except StopIteration: return false...sehingga tidak luput pengecualian.
Ben Jackson
@ Ben: QuerySet.__nonzero__()tidak pernah dipanggil karena QuerySetdikonversi ke listsebelum memeriksa kebenaran. Pengecualian lain mungkin masih terjadi.
Ignacio Vazquez-Abrams
@Ron: Itu bisa menghasilkan StopIterationpengecualian.
Ignacio Vazquez-Abrams
mengkonversi ke daftar === panggilan __iter__untuk mendapatkan objek iterator baru dan memanggil nextmetode itu sampai StopIterationdibuang. Jadi secara pasti akan ada pengecualian di suatu tempat;)
lqc
14
Jawaban ini sekarang kedaluwarsa, lihat @ cod3monk3y jawaban untuk Django 1.6+
ValAyal
37

Sekarang, di Django 1.9 Anda memiliki first() metode untuk kueryset.

YourModel.objects.all().first()

Ini adalah cara yang lebih baik daripada .get()atau [0]karena itu tidak memberikan pengecualian jika queryset kosong, Therafore, Anda tidak perlu memeriksa menggunakanexists()

pungutan
sumber
1
Ini menyebabkan LIMIT 1 di SQL dan saya telah melihat klaim bahwa itu dapat membuat kueri menjadi lebih lambat - walaupun saya ingin melihat yang dibuktikan: Jika kueri hanya mengembalikan satu item, mengapa LIMIT 1 benar-benar memengaruhi kinerja? Jadi saya pikir jawaban di atas baik-baik saja, tetapi akan senang melihat bukti yang mengkonfirmasi.
rrauenza
Saya tidak akan mengatakan "lebih baik". Itu sangat tergantung pada harapan Anda.
trigras
7

Jika Anda berencana untuk mendapatkan elemen pertama sering - Anda dapat memperluas QuerySet ke arah ini:

class FirstQuerySet(models.query.QuerySet):
    def first(self):
        return self[0]


class ManagerWithFirstQuery(models.Manager):
    def get_query_set(self):
        return FirstQuerySet(self.model)

Tentukan model seperti ini:

class MyModel(models.Model):
    objects = ManagerWithFirstQuery()

Dan gunakan seperti ini:

 first_object = MyModel.objects.filter(x=100).first()
Nikolay Fominyh
sumber
Panggil objek = ManagerWithFirstQuery sebagai objek = ManagerWithFirstQuery () - DONT FORGET PARENTHESES - lagi pula, Anda membantu saya jadi +1
Kamil
7

Ini bisa bekerja juga:

def get_first_element(MyModel):
    my_query = MyModel.objects.all()
    return my_query[:1]

jika kosong, maka mengembalikan daftar kosong, jika tidak mengembalikan elemen pertama di dalam daftar.

Nick Cuevas
sumber
1
Sejauh ini, ini adalah solusi terbaik ... hanya menghasilkan satu panggilan ke basis data
Shh
5

Bisa seperti ini

obj = model.objects.filter(id=emp_id)[0]

atau

obj = model.objects.latest('id')
Tariq Nauman
sumber
3

Anda harus menggunakan metode Django, seperti ada. Ada di sana bagi Anda untuk menggunakannya.

if qs.exists():
    return qs[0]
return None
Ari
sumber
1
Kecuali, jika saya mengerti dengan benar, Python idiomatik biasanya menggunakan pendekatan yang lebih mudah untuk meminta maaf daripada izin ( EAFP ) daripada pendekatan Look Before You Leap .
BigSmoke
EAFP bukan hanya rekomendasi gaya, ia memiliki alasan (misalnya, memeriksa sebelum membuka file tidak mencegah kesalahan). Di sini saya pikir pertimbangan yang relevan adalah ada + dapatkan item menyebabkan dua permintaan basis data, yang mungkin tidak diinginkan tergantung pada proyek dan tampilan.
Éric Araujo
2

Sejak django 1.6 Anda dapat menggunakan filter () dengan metode first () seperti:

Model.objects.filter(field_name=some_param).first()
dtar
sumber