A maneira mais rápida de obter o primeiro objeto de um conjunto de consultas no django?

193

Freqüentemente me pego querendo pegar o primeiro objeto de um queryyset no Django, ou retornar Nonese não houver nenhum. Existem muitas maneiras de fazer isso que funcionam. Mas estou me perguntando qual é o mais eficiente.

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

Isso resulta em duas chamadas ao banco de dados? Isso parece um desperdício. Isso é mais rápido?

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

Outra opção seria:

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

Isso gera uma única chamada ao banco de dados, o que é bom. Mas requer a criação de um objeto de exceção a maior parte do tempo, o que exige muita memória quando tudo o que você realmente precisa é de um teste-se trivial.

Como posso fazer isso com apenas uma chamada de banco de dados e sem agitar a memória com objetos de exceção?

Leopd
fonte
21
Regra prática: se você está preocupado em minimizar as viagens de ida e volta ao banco de dados, não use len()em conjuntos de consultas, sempre use .count().
Daniel DiPaolo
7
"criando um objeto de exceção a maior parte do tempo, o que consome muita memória" - se você estiver preocupado em criar uma exceção extra, estará fazendo algo errado, pois o Python usa exceções em todo o lugar. Você realmente comparou que consome muita memória no seu caso?
Lqc
1
@ Leopd E se você realmente comparasse a resposta de alguma forma (ou pelo menos os comentários), saberia que não é mais rápido. Na verdade, pode ser mais lento, porque você cria uma lista extra apenas para jogá-la fora. E tudo isso são apenas amendoins em comparação com o custo de chamar uma função python ou usar o ORM do Django em primeiro lugar! Uma única chamada para filter () é muitas, muitas e muitas vezes mais lenta do que uma exceção (que ainda será gerada, porque é assim que o protocolo do iterador funciona!).
Lqc
1
Sua intuição está correta de que a diferença de desempenho é pequena, mas sua conclusão está errada. Fiz um benchmark e a resposta aceita é de fato mais rápida por uma margem real. Vai saber.
8114 Leopd
11
Para pessoas usando Django 1.6, eles finalmente aditados os first()e last()conveniência métodos: docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Respostas:

327

O Django 1.6 (lançado em novembro de 2013) introduziu os métodos de conveniência first() e last()que engolem a exceção resultante e retornam Nonese o conjunto de consultas não retornar objetos.

cod3monk3y
fonte
1
não faz o [: 1], por isso não é tão rápido (a menos que você precise avaliar todo o conjunto de consultas).
janek37
13
Além disso, first()e last()imponha uma ORDER BYcláusula em uma consulta. Isso tornará os resultados determinísticos, mas provavelmente reduzirá a velocidade da consulta.
Phil Krylov 27/06
@ janek37 não há diferenças no desempenho. Conforme indicado por cod3monk3y, é um método conveniente e não lê todo o conjunto de consultas.
Zompa
142

A resposta correta é

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

Que pode ser usado em:

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

Você não gostaria de transformá-lo em uma lista, porque isso forçaria uma chamada de banco de dados completa de todos os registros. Basta fazer o acima e ele puxará apenas o primeiro. Você pode até usar .order_bypara garantir o primeiro que deseja.

Certifique-se de adicionar o .get()ou você receberá um QuerySet de volta e não um objeto.

stormlifter
fonte
9
Você ainda precisaria envolvê-lo em uma tentativa ... exceto ObjectDoesNotExist, que é como a terceira opção original, mas com fatias.
Danny W. Adair
1
Qual é o objetivo de definir um LIMIT se você chamar call () no final? Deixe o ORM e o compilador SQL decidirem o que é melhor para o back-end (por exemplo, no Oracle Django emula LIMIT, para que seja prejudicial ao invés de ajudar).
Lqc
Eu usei essa resposta sem o .get () à direita. Se uma lista for retornada, retornarei o primeiro elemento da lista.
Keith John Hutchison,
qual é a diferença de ter Entry.objects.all()[0]??
James Lin
15
@JamesLin A diferença é que [: 1] .get () gera DoesNotExist, enquanto [0] gera IndexError.
Ropez 6/09
49
r = list(qs[:1])
if r:
  return r[0]
return None
Ignacio Vazquez-Abrams
fonte
1
Se você ativar o rastreamento, tenho certeza de que você verá isso adicionar LIMIT 1à consulta e não sei se você pode fazer melhor do que isso. No entanto, internamente __nonzero__na QuerySeté implementado como try: iter(self).next() except StopIteration: return false...para que ele não escapa a exceção.
Ben Jackson
@ Ben: QuerySet.__nonzero__()nunca é chamado, pois o QuerySeté convertido em um listantes de verificar a veracidade. Outras exceções ainda podem ocorrer no entanto.
Ignacio Vazquez-Abrams
@ Aron: Isso pode gerar uma StopIterationexceção.
Ignacio Vazquez-Abrams
converter para lista === chamada __iter__para obter um novo objeto iterador e chamar seu nextmétodo até que StopIterationseja lançada. Então, definitivamente não vai ser uma exceção em algum lugar;)
LQC
14
Esta resposta está desatualizada, dê uma olhada na resposta @ cod3monk3y para Django
1.6+
37

Agora, no Django 1.9, você tem um first() método para conjuntos de consultas.

YourModel.objects.all().first()

Essa é uma maneira melhor do que .get()ou [0]porque ela não gera uma exceção se o conjunto de consultas estiver vazio. Portanto, você não precisa verificar usandoexists()

levi
fonte
1
Isso causa um LIMIT 1 no SQL e vi alegações de que pode tornar a consulta mais lenta - embora eu queira ver isso comprovado: se a consulta retorna apenas um item, por que o LIMIT 1 realmente afeta o desempenho? Então eu acho que a resposta acima é boa, mas gostaria de ver evidências se confirmando.
Rruenza
Eu não diria "melhor". Realmente depende de suas expectativas.
trigras 09/04
7

Se você planeja obter o primeiro elemento com frequência - você pode estender o QuerySet nesta direção:

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


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

Defina o modelo assim:

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

E use-o assim:

 first_object = MyModel.objects.filter(x=100).first()
Nikolay Fominyh
fonte
Chamada objetos = ManagerWithFirstQuery como objetos = ManagerWithFirstQuery () - NÃO ESQUEÇA PARENTHESES - de qualquer maneira, você me ajudou +1
Kamil
7

Isso também poderia funcionar:

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

se estiver vazio, retornará uma lista vazia, caso contrário, ele retornará o primeiro elemento dentro de uma lista.

Nick Cuevas
fonte
1
Esta é, de longe, a melhor solução ... resulta em apenas uma chamada para o banco de dados
Shh
5

Pode ser assim

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

ou

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

Você deve usar métodos django, como existe. Está lá para você usá-lo.

if qs.exists():
    return qs[0]
return None
Ari
fonte
1
Exceto que, se eu entendi direito, o Python idiomático geralmente usa uma abordagem Mais fácil de pedir perdão do que permissão ( EAFP ), em vez da abordagem Look Before You Leap .
BigSmoke 12/01
O EAFP não é apenas uma recomendação de estilo, tem motivos (por exemplo, verificar antes de abrir um arquivo não evita erros). Aqui eu acho que a consideração relevante é que existe + get item causa duas consultas ao banco de dados, que podem ser indesejáveis ​​dependendo do projeto e da visualização.
Éric Araujo
2

Desde o django 1.6, você pode usar filter () com o método first () da seguinte forma:

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