tkhyn / django-gm2m

MIT License
36 stars 23 forks source link

Filtering by GM2MField values #31

Closed tkhyn closed 7 years ago

tkhyn commented 8 years ago

Original report by Nick Guziy (Bitbucket: [Nick Guzy](https://bitbucket.org/Nick Guzy), ).


Hi Thomas! I can't figure out if I can filter model with GM2MField by its value, like I can do with django's GenericForeignKey.

Here is my case:

#!python

class Option(models.Model):
   ....

class Main(models.Model):
    options = GM2MField()

option = Option.objects.create()

what I want to achieve is to get all instances of Main model, that have option in the options GM2MField i.e.

#!python

Main.objects.filter(options=option)

this code raises Exception

#!python

FieldError: Field 'options' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.

Can you please help me? Should I manually add a relation (from gm2m/relations.py) to Option class or am I making invalid lookup?

tkhyn commented 8 years ago

Original comment by Thomas Khyn (Bitbucket: tkhyn, GitHub: tkhyn).


Hi Nick, thanks for the report.

This is an issue you'll have if you use a GenericForeignKey as well, it's not specifically django-gm2m related.

Indeed, this message (a bit cryptically I agree) tells you there is no way for django to query the database when it is not explicitly told in which table(s) to look for the data, and that you should instead use a GenericRelation if you are using a GenericForeignKey.

In our case, when using django-gm2m, the end of the message should read: If it is a GM2MField, consider using the automatically created GM2MRelation.

Concretely, that means that instead of doing:

class Option(models.Model):
    ....

class Main(models.Model):
    options = GM2MField()

option = Option.objects.create()
mains_with_option = Main.objects.filter(options=option)

You should instead do:

class Option(models.Model):
    ....

class Main(models.Model):
    # adding Option creates the reverse relation from Option to Main, called main_set
    options = GM2MField(Option) 

option = Option.objects.create()
mains_with_option = option.main_set.all()

And you'll have the expected result.

With the second method, Django knows in which tables to look for the data (Option's and Main's), starting from Option's. In the first case, it knew it had to start from Main's table, and had no explicit indication where to look for the related object.

There is really not much I can do on my side to enable this feature, as this is a django limitation with generic relations. What I can do is add a § in the documentation warnings about this, as the django error message is misleading for django-gm2m users.

EDIT: typo

tkhyn commented 8 years ago

Original comment by Nick Guziy (Bitbucket: [Nick Guzy](https://bitbucket.org/Nick Guzy), ).


Thank you. It's clear now that I should use

#!python

mains_with_option = option.main_set.all()

if I need all mains with selected option. But what should I do if I need to get all instances of Main model that have options = [option1, option2] if I can't do

#!python

mains_with_options = Main.objects.filter(options=[option1, option2])
tkhyn commented 8 years ago

Original comment by Thomas Khyn (Bitbucket: tkhyn, GitHub: tkhyn).


Well you'd simply need to do a normal for loop:

mains_with_options = set()
for option in [option1, option2]:
    mains_with_options.update(option.main_set.all())

This 'looks' inefficient (as many queries as passed options), but even it it were possible to do Main.objects.filter(options__in=[option1, option2]) it would have to do exactly the same thing (one query for each passed option) ...

EDIT: using set instead of [] to avoid duplicates in the result

tkhyn commented 8 years ago

Original comment by Thomas Khyn (Bitbucket: tkhyn, GitHub: tkhyn).


@Nick-G did my suggestion solve your issue?

tkhyn commented 7 years ago

Original comment by Thomas Khyn (Bitbucket: tkhyn, GitHub: tkhyn).


Closing as resolved. Feel free to reopen if the issue you had is still current.

eriktelepovsky commented 2 years ago

Hello. The following goal:

mains_with_options = Main.objects.filter(options=[option1, option2])

can be also achieved this way:

option_ct = ContentType.objects.get_for_model(Option)
im = create_gm2m_intermediary_model(getattr(Main, 'options').field, Main)
ids_of_mains_with_options = set(im.objects.filter(gm2m_ct_id=option_ct.id, gm2m_pk__in=[option1.pk, option2.pk]).values_list('gm2m_src_id', flat=True))
mains_with_options = Main.objects.filter(id__in=ids_of_mains_with_options)