Skip to content

Fixed #9475 -- Allowed RelatedManager.add(), create(), etc. for m2m with a through model.#8981

Merged
timgraham merged 1 commit intodjango:masterfrom
collinanderson:ticket9475
Jan 15, 2019
Merged

Fixed #9475 -- Allowed RelatedManager.add(), create(), etc. for m2m with a through model.#8981
timgraham merged 1 commit intodjango:masterfrom
collinanderson:ticket9475

Conversation

@collinanderson
Copy link
Copy Markdown
Contributor

@collinanderson collinanderson commented Aug 26, 2017

@collinanderson collinanderson force-pushed the ticket9475 branch 2 times, most recently from 066b8ae to e42d74d Compare August 26, 2017 15:56
@claudep
Copy link
Copy Markdown
Member

claudep commented Aug 28, 2017

Very promising!
Do we have a test where the through table requires values and through_defaults is empty?

@collinanderson collinanderson force-pushed the ticket9475 branch 2 times, most recently from 967256d to e0be1e9 Compare September 7, 2017 01:02
@collinanderson
Copy link
Copy Markdown
Contributor Author

Ok I added some docs and added a test for a case where the through table requires a value.

@collinanderson collinanderson force-pushed the ticket9475 branch 2 times, most recently from 55f841d to 151e5c1 Compare January 16, 2018 16:20
@airstandley
Copy link
Copy Markdown

Shouldn't line 955 be self.add(*objs, through_defaults=through_defaults)?

airstandley pushed a commit to LinearSystemsInc/django that referenced this pull request Jan 16, 2018
…anyManager because that seems easier than trying to fix the problem in __set__ which is deprecated for 2.0
@collinanderson
Copy link
Copy Markdown
Contributor Author

@airstandley Nice catch! Fixed.

Copy link
Copy Markdown
Contributor

@auvipy auvipy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!!

@collinanderson collinanderson force-pushed the ticket9475 branch 3 times, most recently from b674ee5 to d1ddf46 Compare January 9, 2019 19:19
@collinanderson
Copy link
Copy Markdown
Contributor Author

I just rebased this patch, updated version numbers, and added fail and success tests for add, create, set, get_or_create, and update_or_create. The failed mariadb test looks like a fluke.

@timgraham
Copy link
Copy Markdown
Member

How about some helpful message if through_defaults is required but not specified (or specified insufficiently or incorrectly)?

@claudep
Copy link
Copy Markdown
Member

claudep commented Jan 9, 2019

Thanks Collin! This would be a nice addition for 2.2, hopefully we can commit it before feature freeze, even if we have to polish some aspects during the alpha phase.

@collinanderson
Copy link
Copy Markdown
Contributor Author

collinanderson commented Jan 9, 2019

Hmm... I don't think it's possible to detect before the fact if through_defaults is needed, because the field could get auto filled in via a custom save() method, so the only way to know if it will work is to catch the error. But, it seems wrong to me to catch the IntegrityError and then raise a different error.

I'm hoping that letting the error message IntegrityError: NOT NULL constraint failed: myapp_mymodel.myrequiredfield bubble up is clear enough.

But maybe catching and raising a new error is helpful? Do you think something like the code below would help?

try:
    self.through._default_manager.using(db).bulk_create(**etc)
except IntegrityError:
    if rel.through._meta.auto_created:
        raise
    raise IntegrityError("Try specifying through_defaults or giving your field a default value")

@collinanderson
Copy link
Copy Markdown
Contributor Author

collinanderson commented Jan 9, 2019

Ohh, I see now we're using bulk_create so values can't be filled in using a custom save() method...

Even still, I don't see a clean way of detecting the issue beforehand.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these tests checking something that the m2m_through tests aren't? It looks odd that the results of these calls aren't verified.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point. I was mostly just converting "can't" test into "can" tests. Should I just delete them these?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look closely but I don't think they add anything (but please audit test coverage as I said in my other comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I removed these tests.

@timgraham timgraham changed the title Fixed #9475 -- allow add(), create(), etc for m2m with through Fixed #9475 -- Allowed RelatedManager.add(), create(), etc. for m2m with a through model. Jan 15, 2019
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check test coverage carefully. I commented out through_defaults here and don't see a test failure.

Copy link
Copy Markdown
Contributor Author

@collinanderson collinanderson Jan 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, turns out there's still a "fields.E332 Many-to-many fields with intermediate tables must not be symmetrical." check which i'll just leave in place. So, I removed the through_defaults on this code path.

@timgraham timgraham merged commit 769355c into django:master Jan 15, 2019
Copy link
Copy Markdown
Member

@ngnpope ngnpope left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few last minute observations...

# If this is a symmetrical m2m relation to self, add the mirror
# entry in the m2m table. `through_defaults` aren't used here
# because of the system check error fields.E332: Many-to-many
# fields with intermediate tables mus not be symmetrical.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

musmust.

# Add the ones that aren't there already
self.through._default_manager.using(db).bulk_create([
self.through(**{
self.through(**dict(through_defaults, **{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can replace **dict(through_defaults, **{…}) and use unpacking generalisations: **{**through_defaults, **{…}}.

Copy link
Copy Markdown
Member

@charettes charettes Jan 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess **through_defaults, **{…} would work as well given surfacing a TypeError if any of the ids properties are provided through through_defaults is probably a better idea than simply dropping them.

"intermediary model. Use %s.%s's Manager instead." %
(opts.app_label, opts.object_name)
)
def add(self, *objs, through_defaults=None):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that we use keyword-only arguments for def set(self, objs, *, clear=False, through_defaults=None):

Do we want to so something similar for other methods:

  • def add(self, *objs, *, through_defaults=None):
  • def create(self, *, through_defaults=None, **kwargs):
  • def get_or_create(self, *, through_defaults=None, **kwargs):
  • def update_or_create(self, *, through_defaults=None, **kwargs):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will push those change, except the suggestion for add() gives invalid syntax.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Strange. Not sure why that doesn't work. Thanks.

# Add the ones that aren't there already
self.through._default_manager.using(db).bulk_create([
self.through(**{
self.through(**dict(through_defaults, **{
Copy link
Copy Markdown
Member

@charettes charettes Jan 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess **through_defaults, **{…} would work as well given surfacing a TypeError if any of the ids properties are provided through through_defaults is probably a better idea than simply dropping them.

self.through(**{
self.through(**dict(through_defaults, **{
'%s_id' % source_field_name: self.related_val[0],
'%s_id' % target_field_name: obj_id,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the %s_id is probably going to break for fields that define different db_columns. This wasn't an issue before because this branch could only be reached with auto-created intemediary models but now that we allow all forms of through we should use attname or directly *_name here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My assumptions here seems to be wrong, this should work fine.

def get_attname(self):
return '%s_id' % self.name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants