1

Django is not managing our databases for us, therefor I created the table RulesetRuleMap to handle the ManyToMany relationship between Ruleset and Rule: Each Ruleset can consist of multiple Rules and each Rule can be used in multiple Rulesets.

Models

class Rule(models.Model):
    id = models.BigAutoField(primary_key=True)
    percentage_of_total = models.FloatField(blank=False, null=False)
    _rule_parameter = models.ForeignKey('RuleParameter', models.DO_NOTHING, blank=False, null=False)

    class Meta:
        managed = False
        db_table = '_rule'


class Ruleset(models.Model):
    id = models.BigAutoField(primary_key=True)
    name = models.CharField(max_length=300, blank=False, null=False)
    description = models.CharField(max_length=300, blank=False, null=False)
    rules = models.ManyToManyField('Rule', through="RulesetRuleMap")

    class Meta:
        managed = False
        db_table = '_ruleset'


class RulesetRuleMap(models.Model):
    id = models.BigAutoField(primary_key=True)
    _rule = models.ForeignKey('Rule', models.CASCADE)
    _ruleset = models.ForeignKey('Ruleset', models.CASCADE)

    class Meta:
        managed = False
        db_table = '_ruleset_rule_map'

Serializers

class RulesetRuleMapSerializer(serializers.ModelSerializer):
    class Meta:
        model = db_models.RulesetRuleMap
        fields = '__all__'


class RuleSerializer(serializers.ModelSerializer):
    class Meta:
        model = db_models.Rule
        fields = '__all__'


class RulesetSerializer(serializers.ModelSerializer):
    rules = RuleSerializer(many=True)
    class Meta:
        model = db_models.Ruleset
        fields = '__all__'

    def create(self, validated_data):
        rules_data = validated_data.pop('rules')
        ruleset = db_models.Ruleset.objects.create(**validated_data)
        rules_storage =[]
        for rule_data in rules_data:
            rule, created = db_models.Rule.objects.get_or_create(**rule_data)
            rules_storage.append(rule)
        ruleset.rules.add(*rules_storage, through_defaults={})
        return ruleset

On a homepage the user can add/modify a Ruleset and add/modify the assosiated Rules. On submission we receive a payload like this:

{
  "id": None,
  "name": "Split_50.0_Param1_50.0_Param2",
  "description": "test",
  "rules": [
    {
      "id": None,
      "percentage_of_total": "50",
      "tc_rule_parameter": "3"
    },
    {
      "id": None,
      "percentage_of_total": "50",
      "tc_rule_parameter": "2"
    }
  ]
}

As described in Djange REST Framework I defined a custom create() for the nested RulesetSerializer to handle the creation of multiple objects. According to Django one should be able to

use add(), create(), or set() to create relationships, as long as you specify through_defaults for any required fields.

When executing ruleset.rules.add(*rules_storage, through_defaults={}) I get the error

{TypeError}add() got an unexpected keyword argument 'through_defaults'

When executing ruleset.rules.add(*rules_storage) I get the error

{AttributeError}Cannot use add() on a ManyToManyField which specifies an intermediary model.Use database_models.TcRulesetRuleMap's Manager instead.

Is there a mistake in my model and/or serializer set up or is there a bug in django?

P. Maino
  • 303
  • 1
  • 3
  • 11

2 Answers2

0

As it says there:

Cannot use add() on a ManyToManyField which specifies an intermediary model.

You've specified your own through model, RulesetRuleMap, so you need to create the objects yourself because django doesn't support add() for this scenario.

If you want to use add(), create() or set() then, according to the documentation listed below, you need to pass the kwarg through_defaults for any required fields. You don't have any extra required fields, so debugging on your specific set of models would be required.

The DRF docs for this relationship with a through model are here

There's a good example in the django docs for setting up the objects with a through model here

In the example, there are 3 models, Person, Group and Membership. Group has the M2M to Person through Membership.

>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
markwalker_
  • 12,078
  • 7
  • 62
  • 99
  • 1
    Please explain the difference between the django models and mine @markwalker_ . The linked django example specified it's own through model as well and add(), create() or set() can be used on it. Ruleset -> Membership, Rule -> Person , Ruleset -> Group – P. Maino Sep 16 '20 at 13:50
  • There isn't really a difference in the models. They specify two models which are M2M linked through a custom model rather than a magic django M2M model, just like you do. `Rule` is like `Person`, `Ruleset` like `Group` and `RulesetRuleMap` like `Membership`. You're right - the docs also show how you can use some of the builtin methods like `add()` and you don't have any extra required fields, so why django has a problem with what you're doing isn't clear without more debugging of your specific scenario – markwalker_ Sep 16 '20 at 15:22
0

I misread the django docs. I'm on django 2.1 and the through fields were only introduced in 2.2.

P. Maino
  • 303
  • 1
  • 3
  • 11