Question

I have been using Django for a couple of years now but I am struggling today with adding a HAVING constraint to a GROUP BY.

My queryset is the following:

crm_models.Contact.objects\
.filter(dealercontact__dealer__pk__in=(265,),
         dealercontact__activity='gardening',
         date_data_collected__gte=datetime.date(2012,10,1),
         date_data_collected__lt=datetime.date(2013,10,1))\
.annotate(nb_rels=Count('dealercontact'))

which gives me the following MySQL query:

SELECT *
FROM `contact` 
LEFT OUTER JOIN `dealer_contact` ON (`contact`.`id_contact` = `dealer_contact`.`id_contact`) 
WHERE (`dealer_contact`.`active` = True 
   AND `dealer_contact`.`activity` = 'gardening'  
   AND `contact`.`date_data_collected` >= '2012-10-01'  
   AND `contact`.`date_data_collected` < '2013-10-01'
   AND `dealer_contact`.`id_dealer` IN (265)) 
GROUP BY `contact`.`id_contact`
ORDER BY NULL;

I would get exactly what I need with this HAVING constraint:

HAVING SUM(IF(`dealer_contact`.`type`='customer', 1, 0)) = 0 

How can I get this fixed with a Django Queryset? I need a queryset in this instance.

Here I am using annotate only in order to get the GROUP BY on contact.id_contact.

Edit: My goal is to get the Contacts who have no "customer" relation in dealercontact but have "ref" relation(s) (according to the WHERE clause of course).

Models

class Contact(models.Model):
    id_contact = models.AutoField(primary_key=True)
    title = models.CharField(max_length=255L, blank=True, choices=choices_custom_sort(TITLE_CHOICES))
    last_name = models.CharField(max_length=255L, blank=True)
    first_name = models.CharField(max_length=255L, blank=True)
    [...]
    date_data_collected = models.DateField(null=True, db_index=True)

class Dealer(models.Model):
    id_dealer = models.AutoField(primary_key=True)
    address1 = models.CharField(max_length=45L, blank=True)
    [...]

class DealerContact(Auditable):
    id_dealer_contact = models.AutoField(primary_key=True)
    contact = models.ForeignKey(Contact, db_column='id_contact')
    dealer = models.ForeignKey(Dealer, db_column='id_dealer')
    activity = models.CharField(max_length=32, choices=choices_custom_sort(ACTIVITIES), db_index=True)
    type = models.CharField(max_length=32, choices=choices_custom_sort(DEALER_CONTACT_TYPE), db_index=True)
Was it helpful?

Solution

I figured this out by adding two binary fields in DealerContact: is_ref and is_customer.

If type='ref' then is_ref=1 and is_customer=0. Else if type='customer' then is_ref=0 and is_customer=1.

Thus, I am now able to use annotate(nb_customers=Sum('is_customer')) and then use filter(nb_customers=0).

The final queryset consists in:

Contact.objects.filter(dealercontact__dealer__pk__in=(265,),  
                       dealercontact__activity='gardening', 
                       date_data_collected__gte=datetime.date(2012,10,1),
                       date_data_collected__lt=datetime.date(2013,10,1))\
               .annotate(nb_customers=Sum('dealercontact__is_customer'))\
               .filter(nb_customers=0)

OTHER TIPS

Actually there is a way you can add your own custom HAVING and GROUP BY clauses if you need.

Just use my example with caution - if Django ORM code/paths will change in future Django versions, you will have to update your code too.

Image you have Book and Edition models, where for each book there can be multiple editions and you want to select first US edition date within Book queryset.

Adding custom HAVING and GROUP BY clauses in Django 1.5+:

from django.db.models import Min
from django.db.models.sql.where import ExtraWhere, AND

qs = Book.objects.all()

# Standard annotate
qs = qs.annotate(first_edition_date=Min("edition__date"))

# Custom HAVING clause, to limit annotation by US country only
qs.query.having.add(ExtraWhere(['"app_edition"."country"=%s'], ["US"]), AND)

# Custom GROUP BY clause will be needed too
qs.query.group_by.append(("app_edition", "country"))

ExtraWhere can contain not just fields, but any raw sql conditions and functions too.

Are you not using raw query just because you want orm object? Using Contact.objects.raw() generate instances similar filter. Refer to https://docs.djangoproject.com/en/dev/topics/db/sql/ for more help.

My goal is to get the Contacts who have no "customer" relation in dealercontact but have "ref" relation(s) (according to the WHERE clause of course).

This simple query fulfills this requirement:

Contact.objects.filter(dealercontact__type="ref").exclude(dealercontact__type="customer")

Is this enough, or do you need it to do something more?

UPDATE: if your requirement is

Contacts that have a "ref" relations, but do not have "customer" relations with the same dealer

you can do this:

from django.db.models import Q
Contact.objects.filter(Q(dealercontact__type="ref") & ~Q(dealercontact__type="customer"))
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top