Reading Extra Fields in a ManyToMany ("through") Table

I’m relatively newish to Django, working on a little project to practise what I’m learning.

I have a table with a ManyToManyField that specifies a “through table” so that I can add a bit more information to the many-to-many relationship. All works fine and I’ve got some test data, no problems for the most part.

I’m stuck now though trying to work out how to read a field from the link in a view function along the following lines – edited for brevity and clarity…

def display_my_thing(request, ref_ident):
    qryset = Catalogue.objects.filter(Q(something...) | Q(more...) | Q(otherthing)).select_related().distinct()
    my_obj = qryset.first()  # For simplicity's sake here.

    caption_html = my_obj.caption  # Ordinary field is fine.
    related_title = my_obj.entry.title  # Regular related field is fine.

    extra_info = my_obj.manytomanyTable.extra_field_name  # <-- No idea how to do this.

    # I'll give an example below based on the Django docs to make things easier.

    pass  # Etc....

So that we can talk about a concrete example without pasting a tonne of code, let’s just use the example in the Django docs here – Models | Django documentation | Django (The Django docs are fabulous btw. A huge resource!)

Let’s suppose I get a Group object like this:
g = Group.objects.filter(...something...).select_related().dictinct().first()

Now, I can inspect g.name, or g.members__person (or something like that, I forget), but what I want to do in my view function is to pull out the “invite_reason”. I’m trying things like the following, but none of them work:
why_invited = g.membership__invite_reason

I can inspect a lot of things with a “manage.py shell” but that’s not solved it for me here so far!

I’m hoping I’ve included a decent enough description to answer what is probably a simple question once you know :slight_smile:

Thanks in advance,
James.

Actually, rather than trying to mix examples from the docs with what you’re trying to do, it’s likely going to be easier if you post your models here.

In general, you retrieve any attribute of any table using the “dot notations”. (The underscore notation only really applies as the parameter to a function such as filter.)

Keep in mind that a ManyToMany relationship means that you can have multiple references to the same tables at both ends. So to access the extra fields, you first need to identify which row you’re interested in, requiring you to filter by both ends of the relationship.

I thought that might be confusing – I started trying to post my example, but would end up with pages of irrelevant noise, I decided the example from the docs made a LOT more sense.

And yes, forgot to use the dot notation. I should have written why_invited = g.membership.invite_reason or some such.

I know SQL pretty well, and I know my filter gets what I want, and the first() grabs the row I’m interested in, which will include a single Many-to-Many record, too, because of how I filter it.

The crux of my question is that (as the docs say), select_related() works for OneToOne and ForeignKey – it says nothing about the ManyToMany case – which, I think, is why I can’t find the way to refer to the “extra fields” in my “through table” for the single row I’m trying to retrieve. Do I need to change the earlier line to explicitly pull those columns into the result?

Je.

Ok, we can work with the docs example. Posted here for clarity:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name

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

    def __str__(self):
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

Using this as the base, what is your question?

1 Like

One item I will clarify - select_related and prefetch_related have nothing to do with access to data. They are performance-enhancements only. They are never needed for any purpose.

1 Like

Oh yes, I forgot I think I read something about them being for performance, thanks for the reminder.

So, yes, my question…

Suppose I have something like:

g = Group.objects.filter(...something...).dictinct().first()

where that “…something…” is some criteria that both (a) give me one Group object and (b) only one relevant many-to-many row from the Membership model/table. I’m familiar with doing that in SQL where I’d have some outer joins to perform selection on my Group table and the linked Membership table to produce a single row in which I have columns from Group and at least a column or two from Membership.

Using Django, I can refer to g.name easily and obviously, but anything I do to try and extract the “invite_reason” column from my single-record result just produces an error.

I can get everything I need from everywhere else in all cases – it’s purely a problem when I try to fetch extra field data from the “through table”.

The closest thing in the aforementioned docs (Models | Django documentation | Django) is where it says the following:

If you need to access a membership’s information you may do so by directly querying the Membership model:

>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
>>> ringos_membership.invite_reason
'Needed a new drummer.'

What I’m trying to do is to get that “invite_reason” for the single row I retrieved in my initial query, which will have filters/criteria that ensure only one many-to-many row is matched for the retrieved Group.

Ok, so the issue is that while you might “know” that there’s only one matching many-to-many row, there’s no “proof” or “assurance” at the database layer that that condition holds.

Any time you have a single Group object, you must always account for the possibility that there’s more than one Membership associated with it.

To that end, if group is an instance of Group, then the set of Membership related to group is group.membership_set. (See Related objects reference | Django documentation | Django).

At that point, you then need to identify which member of that set to access. If you know that there is only one, so that you are not relying upon an indeterminate sort order, your reference to that singular Membership object is group.membership_set.first(). Therefore, the reference to the invite_reason member is group.membership_set.first().invite_reason.

1 Like

Aha! At a quick glance, I think I see it now – I need to use first() twice, once on my result and then again on the membership set… looks promising.

I’ll go try it out and let you know how I get on! :+1:

Fan-bloomin’-tastic…! Thanks @KenWhitesell that’s nailed it! :+1: :+1:

I can see myself fighting the urge to think in terms of the SQL that I’d write… getting there, slowly, bit by bit…!

Part of the reason I often try to construct a single SQL statement that does as much of the work for me as possible is because of race conditions, of course, so I’ll need to hone my understanding of when Django hits the database – I know I see a lot of references to that in the docs, so will continue to live half my waking hours delving into that :slight_smile:

Thanks again for the help and bearing with me where I was finding it tricky to pin down what the issues was!

Je.

I still do - after all, I’ve been writing SQL since 1987.

Fortunately, the Python syntax used by Django allows you chain most expressions together as a single statement if that’s how you want to write them.
But either way, everything you need to know about that topic is summarized by the phrase “Querysets are lazy”.

1 Like

i have a many-to-many to it self

class Article_Version_Child(BaseModel):
    from_article_version = models.ForeignKey(ArticleVersion, on_delete=models.CASCADE, related_name='from_article_version')
    to_article_version = models.ForeignKey(ArticleVersion, on_delete=models.CASCADE, related_name='to_article_version')
    quantity = models.IntegerField(verbose_name="Quantidade", null=True, blank=True, default=1)

i already tried many ways, but i cant get the quantity, i always get “‘ArticleVersion’ object has no attribute ‘article_version_child_set’”

Examples:
article_version.article_version_child_set.all()
article_version.articleversionchild_set.get(from_article_version=OtherArticleVersion)

Anybody know if is possible get Extra Fields in a ManyToMany with self?

This topic has been marked as solved. If you have a question or issue, please open a new topic.

Thanks, done.