Guest User

Custom JOINs with Django's query.join()

a guest
Apr 18th, 2015
466
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 4.08 KB | None | 0 0
  1. Custom JOINs with Django's query.join()
  2.  
  3. Django's ORM is great. It handles simple to fairly complex queries right out the box without having to
  4. write any SQL. If you need a complicated query, Django's lets you use .extra(), and you can always fallback
  5. to raw SQL if need be, but then you lose the ORM's bells and whistles. So it's always nice to find solutions
  6. that allow you to tap into the ORM at different levels.
  7.  
  8. Recently, we were looking to perform a LEFT OUTER JOIN through a Many to Many relationship. For a lack of a better example, let's use a Contact model (crm_contact), which has many Phones (crm_phones):
  9.  
  10. class Contact(models.Model):
  11.     name = models.CharField(max_length=255)
  12.     phones = models.ManyToManyField("Phone")
  13.     addresses = models.ManyToManyField("Address")
  14.  
  15. class Phone(models.Model):
  16.     number = models.CharField(max_length=16)
  17.  
  18. If we want to display each contact and corresponding phone numbers,
  19. looping through each contact in Contact.objects.all() and
  20. following the phones relationship will generate quite a few database queries
  21. (especially with a large contact table). select_related() doesn't work in this scenario either,
  22. because it only supports Foreign Key relationships. We can use extra() to add a
  23. select parameter, but tables=['crm_phones'] will not generate a LEFT OUTER join type.
  24. We need to explicitly construct the JOIN.
  25.  
  26. DISCLAIMER: The following method does work, but should not be considered best practice.
  27. That is, there may be a better way to accomplish the same task (please comment if so!).
  28. But after sparse Google results for similar scenarios, I figured it'd at least be useful
  29. to post what we discovered.
  30. After digging around in django.db.models.sql for a bit, we found BaseQuery.join in query.py.
  31. Among the possible arguments, the most important is connection, which
  32. is "a tuple (lhs, table, lhs_col, col) where 'lhs' is either an existing table alias or a table name.
  33. The join corresponds to the SQL equivalent of:
  34.     lhs.lhs_col = table.col".
  35. Further, the promote keyword argument will set the join type to be a LEFT OUTER JOIN.
  36.  
  37. Now we can explicitly setup the JOINs through crm_contact -> crm_contact_phones -> crm_phone:
  38.  
  39.     contacts = Contact.objects.extra(
  40.         select={"phone": "crm_phone.number"}
  41.     ).order_by("name")
  42.     # setup intial FROM clause
  43.     # OR contacts.query.get_initial_alias()
  44.     contacts.query.join((None, "crm_contact", None, None))
  45.     # join to crm_contact_phones
  46.     connection = (
  47.         "crm_contact",
  48.         "crm_contact_phones",
  49.         "id",
  50.         "contact_id",
  51.     )
  52.     contacts.query.join(connection, promote=True)
  53.     # join to crm_phone
  54.     connection = (
  55.         "crm_contact_phones",
  56.         "crm_phone",
  57.         "phone_id",
  58.         "id",
  59.     )
  60.     contacts.query.join(connection, promote=True)
  61.  
  62. It's a little verbose, but it accomplishes our goal. I used hardcoded table names/columns in the
  63. connection tuple to make it easier to follow, but we can also extract this information from the objects
  64. themselves:
  65.  
  66.     contacts = Contact.objects.extra(
  67.         select={"phone": "crm_phone.number"}
  68.     ).order_by("name")
  69.     # setup intial FROM clause
  70.     # OR contacts.query.get_initial_alias()
  71.     contacts.query.join((None, Contact._meta.db_table, None, None))
  72.     # join to crm_contact_phones
  73.     connection = (
  74.         Contact._meta.db_table,
  75.         # crm_contact
  76.         Contact.phones.field.m2m_db_table(),
  77.         # crm_contact_phones
  78.         Contact._meta.pk.column,
  79.         # etc...
  80.         Contact.phones.field.m2m_column_name(),
  81.     )
  82.     contacts.query.join(connection, promote=True)
  83.     # join to crm_phone
  84.     connection = (
  85.         Contact.phones.field.m2m_db_table(),
  86.         Phone._meta.db_table,
  87.         Contact.phones.field.m2m_reverse_name(),
  88.         Phone._meta.pk.column,
  89.     )
  90.     contacts.query.join(connection, promote=True)
  91.  
  92. This results in a row for each phone number (Cartesian product), but we can print out each
  93. contact and corresponding phone numbers (with a single SQL statement) quickly in a template
  94. using {% ifchanged %}:
  95.  
  96. Contacts
  97. {% for contact in contacts %}
  98.     {% ifchanged contact.name %}
  99.         {{ contact.name }}
  100.     {% endifchanged %}
  101.     Phone: {{ contact.phone }}
  102. {% endfor %}
Advertisement
Add Comment
Please, Sign In to add comment