Search code examples
djangopostgresqldjango-models

Django custom `get_or_create` on model with M2M through model


As title says I'm trying to implement get_or_create method which able to get or create instance with respect to through model.

Models:

class Component(models.Model):
    name = CharField()

class ConfigComponent(models.Model):
    config = models.ForeignKey(Config)
    component = models.ForeignKey(Component)
    quantity = models.PositiveIntegerField()

class Config(models.Model):
    components = models.ManyToManyField(Component, through="ConfigComponent")

So basically I want to check if Config matching input data exists and use it if so. Otherwise - create a new one. But how to consider the quantity of each Component? It is not a big deal to find Config with exact num of components but it may appears that components match but their quantity is different.

def get_or_create(
    self, 
    components: list[tuple[Component, int]]
) -> tuple[Config, bool]:
    
    component_names = [component[0] for component in components]
    components_count = len(components)

    configs_match = Config.objects.annotate(
                                      total_components=Count('components'),
                                      matching_components=Count(
                                         'components',
                                         filter=Q(components__in=component_names)
                                      ))\
                                  .filter(
                                      total_components=components_count, 
                                      matching_components=components_count
                                  )
               

                             

Solution

  • Use:

    from django.db.models import Count, Q
    
    configs_match = Config.objects.alias(
        count=Count('components'),
        nfilter=Count('components', filter=Q(components__in=my_components)),
    ).filter(
        count=len(my_components),
        nfilter=len(components),
    )

    This will look for an exact match, since the Configs need to have the same number of components as len(my_components), and this should also hold for all components in my_components.

    Or to also account for integers:

    from django.db.models import Count, Q
    
    configs_match = Config.objects.alias(
        count=Count('components'),
        nfilter=Count(
            'components',
            filter=Q(
                *[
                    Q(component=c, configcomponent__quantity=q)
                    for c, q in my_components
                ],
                _connector=Q.OR
            ),
        ),
    ).filter(
        count=len(my_components),
        nfilter=len(my_components)),
    )

    with my_components a list of 2-tuples with the first item the Component, and the second item the corresponding quantity.