Como consultar corretamente um ManyToManyField para todos os objetos em uma lista (ou outra ManyToManyField)?
-
12-09-2019 - |
Pergunta
Estou bastante perplexo sobre a melhor maneira de construir uma consulta de Django que verifica se todas os elementos de um campo ManyToMany
(ou uma lista) estão presentes em outro campo ManyToMany
.
Como um exemplo, eu tenho várias Person
s, que podem ter mais de uma especialidade. Há também Job
s que as pessoas podem começar, mas eles exigem um ou mais Specialty
s para ser elegível para ser iniciado.
class Person(models.Model):
name = models.CharField()
specialties = models.ManyToManyField('Specialty')
class Specialty(models.Model):
name = models.CharField()
class Job(models.Model):
required_specialties = models.ManyToManyField('Specialty')
Uma pessoa pode iniciar um trabalho de única , se eles têm todas as especialidades que o trabalho exige. Então, novamente por causa do exemplo, temos três especialidades:
- Codificação
- Singing
- Dancing
E eu tenho um Job
que requer as especialidades de canto e dança. Uma pessoa com canto e dança especialidades pode iniciá-lo, mas outra com Codificação e cantando especialidades não pode -. Como o trabalho exige uma pessoa que pode tanto cantar e dançar
Então, agora eu preciso de uma maneira de encontrar todos os trabalhos que uma pessoa pode assumir. Esta foi a minha maneira de lidar com ele, mas eu tenho certeza que há uma abordagem mais elegante:
def jobs_that_person_can_start(person):
# we start with all jobs
jobs = Job.objects.all()
# find all specialties that this person does not have
specialties_not_in_person = Specialty.objects.exclude(name__in=[s.name for s in person.specialties])
# and exclude jobs that require them
for s in specialties_not_in_person:
jobs = jobs.exclude(specialty=s)
# the ones left should fill the criteria
return jobs.distinct()
Este é porque usar Job.objects.filter(specialty__in=person.specialties.all())
voltará empregos que correspondem qualquer de especialidades da pessoa, não todos eles. Usando esta consulta, o trabalho que exige cantando e dançando apareceria para o codificador de cantar, o que não é a saída desejada.
Eu estou esperando que este exemplo não é muito complicado. A razão que eu estou preocupado com isso é que as especialidades no sistema provavelmente será muito mais, e looping sobre eles não parece ser a melhor maneira de conseguir isso. Eu estou querendo saber se alguém poderia emprestar um arranhão a esta coceira!
Solução
Outra idéia
Ok, eu acho que deveria ter adicionado este para a outra resposta, mas quando eu comecei sobre ele, parecia que ia ser uma direção diferente haha ??
Não há necessidade de iterate:
person_specialties = person.specialties.values_list('pk', flat=True)
non_specialties = Specialties.objects.exclude(pk__in=person_specialties)
jobs = Job.objects.exclude(required_specialties__in=non_specialties)
Nota ??strong>: eu não sei exatamente o quão rápido é isso. Você pode ser melhor fora com as minhas outras sugestões.
Também: Este código não foi testado
Outras dicas
Eu acho que você deve olhar para usando values_list para obter especialidades da pessoa
Substituir:
[s.name for s in person.specialties]
com:
person.specialties.values_list('name', flat=True)
Isso vai lhe dar uma lista simples (ie. [ 'Spec1', 'spec2', ...]) que você pode usar novamente. E a consulta SQL usada no bg também será mais rápido porque ele só irá selecionar 'nome' em vez de fazer uma select *
para preencher os objetos ORM
Você também pode obter uma melhoria de velocidade por empregos de filtragem que a pessoa definitivamente não pode executar:
para substituir:
jobs = Job.objects.all()
com (2 consultas - obras para Django 1.0 +)
person_specialties = person.specialties.values_list('id', flat=True)
jobs = Job.objects.filter(required_specialties__id__in=person_specialties)
ou com (1 query -? Trabalha para django1.1 +)
jobs = Job.objects.filter(required_specialties__in=person.specialties.all())
Você também pode obter uma melhoria usando select_related () em seu emprego pesquisas / pessoa (uma vez que têm uma chave estrangeira que você está usando)