Django making a list of a field grouping by another field in model

In case your Django is backed by PostgreSQL, you can use extra in combination with string_agg.

https://docs.djangoproject.com/en/1.8/ref/models/querysets/#extra http://www.postgresql.org/docs/9.4/static/sql-expressions.html#SYNTAX-AGGREGATES

Assuming the table you posted is a ManyToManyRelation with item actually being a reference to your model MyModel, with the model table being mymodel and the mapping table being mymodel_value:

MyModel.objects.extra(select={
    'values':
        """
        SELECT string_agg(value, ', ' ORDER BY value)
          FROM mymodel_value
         WHERE mymodel.id=mymodel_value.item
        """
}).values('values')

The resulting dictionary will have an entry values with a string as value that is the concatenated (aggregated) list of values for each item.

Try this in the Django shell (./manage.py shell). You might have to add more tables to the subselect in case those have not been added by the ORM, yet. (The main model table should definitely be there, already.) It depends on how complex the model relations are.

Turn on DB logging to inspect the query that the ORM generates.


(Apr 4 '16) UPDATE: This is a working solution for Django <= 1.7. For newer versions please read Creating your own Aggregate Functions from the docs.

Using a custom Concat aggregate taken from here (an article about the topic)

Define this:

class Concat(models.Aggregate):
    def add_to_query(self, query, alias, col, source, is_summary):
        #we send source=CharField to prevent Django from casting string to int
        aggregate = SQLConcat(col, source=models.CharField(), is_summary=is_summary, **self.extra)
        query.aggregates[alias] = aggregate

#for mysql
class SQLConcat(models.sql.aggregates.Aggregate):
    sql_function = 'group_concat'

    @property
    def sql_template(self):
        if self.extra.get('separator'):
            return '%(function)s(%(field)s SEPARATOR "%(separator)s")'
        else:
            return '%(function)s(%(field)s)'

#For PostgreSQL >= 9.0
#Aways use with separator, e.g. .annotate(values=Concat('value', separator=','))     
class SQLConcat(models.sql.aggregates.Aggregate):
    sql_function = 'string_agg'

    @property
    def sql_template(self):
        #the ::text cast is a hardcoded hack to work with integer columns
        return "%(function)s(%(field)s::text, '%(separator)s')"

#For PostgreSQL >= 8.4 and < 9.0
#Aways use with separator, e.g. .annotate(values=Concat('value', separator=','))     
class SQLConcat(models.sql.aggregates.Aggregate):
    sql_function = 'array_to_string'

    @property
    def sql_template(self):
        return "%(function)s(array_agg(%(field)s), '%(separator)s')"

#For PostgreSQL < 8.4 you should define array_agg before using it:
#CREATE AGGREGATE array_agg (anyelement)
#(
#    sfunc = array_append,
#    stype = anyarray,
#    initcond = '{}'
#);

class MyModel(models.Model):
    item = models.CharField(max_length = 255)
    date = models.DateTimeField()
    value = models.IntegerField()

so now you can do:

>>> from my_app.models import MyModel, Concat
>>> MyModel.objects.values('item').annotate(values=Concat('value'))
[{'item': u'ab', 'values': u'124,433,99'}, {'item': u'abc', 'values': u'23,80'}]

to get values as a list of integers you need to manually .split and cast to int. Something like:

>>> my_list = MyModel.objects.values('item').annotate(values=Concat('value'))
>>> for i in my_list:
...     i['values'] = [int(v) for v in i['values'].split(',')]
...
>>> my_list
[{'item': u'ab', 'values': [124, 433, 99]}, {'item': u'abc', 'values': [23, 80]}]