#21763 closed Cleanup/optimization (fixed)

Misleading Error: 'ManyRelatedManager' object has no attribute 'add'

Reported by: anonymous Owned by: anonymous
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: no
Easy pickings: yes UI/UX: no

Description (last modified by charettes)

Synopsis
When defining a many-to-many relationship using a 'through' model, the 'add' and 'create' methods are not available, for good reason. However, rather than failing with a helpful exception directing the user to use the through model, the relevant ManyRelatedManager is simply missing its usual methods, raising a misleading AttributeError instead.

Expected Behavior
IMO, it would be more helpful, and possibly somewhat more Pythonic, to produce a type of exception that directs the user to use the through model. This correctly indicates the source of the problem, and doesn't require much additional monkeying about since the object already has to know of the need to use one as it is now, since it must be instantiated with no add/create methods.

Actual Behavior
The ManyRelatedManager is created missing two of its usual methods, raising an AttributeError if they are called. This is misleading[1] and can result in several wasted minutes making sure the method name and object is correct, consulting documentation, and so on.

Steps to Reproduce

  1. Create two simple models.
  2. Link them with a third model defined as a 'through' relation.
  3. Call add or create on one of the ManyRelatedManagers.

Example:

# in models.py
from django.db import models

class Person(models.Model):
    pass

class Group(models.Model):
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField(auto_now_add=True)
# on shell
>>> from testing.models import *
>>> group = Group.objects.create()
>>> person = Person.objects.create()
>>> group.members.add(person)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'ManyRelatedManager' object has no attribute 'add'}}}

[1] See, e.g., [this StackOverflow question](http://stackoverflow.com/q/8095813/2588818) and the related top comment/answer.

Change History (7)

comment:1 Changed 17 months ago by seregon@…

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

comment:2 Changed 17 months ago by charettes

  • Component changed from Uncategorized to Database layer (models, ORM)
  • Description modified (diff)
  • Easy pickings set
  • Needs tests set
  • Triage Stage changed from Unreviewed to Accepted
  • Type changed from Uncategorized to Cleanup/optimization
  • Version changed from 1.5 to master

I agree that raising a more significant AttributeError wouldn't hurt.

comment:3 Changed 17 months ago by oflores

Hi, it seems to be an expected behavior and very well documented. Here it is part of the explanation from django doc.

https://docs.djangoproject.com/en/1.6/topics/db/models/#extra-fields-on-many-to-many-relationships

"Unlike normal many-to-many fields, you can’t use add, create, or assignment (i.e., beatles.members = [...]) to create relationships:

# THIS WILL NOT WORK
>>> beatles.members.add(john)
# NEITHER WILL THIS
>>> beatles.members.create(name="George Harrison")
# AND NEITHER WILL THIS
>>> beatles.members = [john, paul, ringo, george]

Why? You can’t just create a relationship between a Person and a Group - you need to specify all the detail for the relationship required by the Membership model. The simple add, create and assignment calls don’t provide a way to specify this extra detail. As a result, they are disabled for many-to-many relationships that use an intermediate model. The only way to create this type of relationship is to create instances of the intermediate model."

comment:4 Changed 16 months ago by timo

Looks like the file to modify here will be django/db/models/fields/related.py. In particular, look for the comment "If the ManyToMany relation has an intermediary model". The if rel.through._meta.auto_created guard that currently prevents add() and remove() from being added should be moved inside those methods and an exception raised something like: raise AttributeError("add() is disabled for ManyToManyFields with through() models.") It looks like create() already throws an AttributeError with a message. I haven't looked into if we can do something similar for direct assignment (the 3rd case in the above comment).

comment:5 Changed 16 months ago by rkstapenhurst

  • Owner changed from nobody to anonymous
  • Status changed from new to assigned

comment:6 Changed 16 months ago by rkstapenhurst

  • Has patch set

Pull request here: https://github.com/django/django/pull/2253

Since create() and set already raise suitably descriptive AttributeErrors, I've altered add() and remove() to do the same.

comment:7 Changed 16 months ago by Tim Graham <timograham@…>

  • Resolution set to fixed
  • Status changed from assigned to closed

In 12385a5f868ee697266756511a85434627ae7ca6:

Fixed #21763 -- Added an error msg for missing methods on ManyRelatedManager.

Attempting to add() and remove() an object related by a 'through' model
now raises more descriptive AttributeErrors, in line with set and
create().

Note: See TracTickets for help on using tickets.
Back to Top