Django Many to Many (through) with three tables and extra attributes

Question:

I have an already existing database with 3 tables: Person, Card, and Permission.
A user may have access to multiple cards, of which only some is owned by the person.
How would I get all cards that the person has access to with the ownership status?
Due to how the system should work, it’s essential that the DB fetch is done in a single query. It’s as if the usage of some magic values (such as objects.<table_name>_set….) is necessary to get this working?

Ideal target query (including the person table) would be:

SELECT card.id, card.text, permissions.owner
FROM person.id JOIN permissions ON person.id = permissions.person_id JOIN cards.id = permissions.card_id
WHERE person.id = <some person id>;

Example target result from the query fetching cards for user id=1:

[
  {
    "id": 123,
    "text": "some random text",
    "owner": false
  },
  {
    "id": 682,
    "text": "more random text",
    "owner": true
  }
]

I’ve also tried some other solutions in some other threads, but most of them either don’t include the extra data from the through-table, or don’t work as they seem to require an extra table (such as Many-to-many relationship between 3 tables in Django which will throw an error "relation personcardpermissions does not exist" when attempting to construct the extra model).

Database table creation SQL:

CREATE TABLE person (
    id SERIAL NOT NULL,
    name VARCHAR(5) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE card (
    id SERIAL NOT NULL,
    text VARCHAR(64) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE permissions (
    person_id INT NOT NULL,
    card_id INT NOT NULL,
    owner BOOLEAN NOT NULL,
    FOREIGN KEY (person_id) REFERENCES person(id),
    FOREIGN KEY (card_id) REFERENCES card(id),
    PRIMARY KEY (person_id, card_id)
);

Currently used models:

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

    class Meta:
        managed = False
        db_table = 'person'


class Card(models.Model):
    text = models.CharField(max_length=64)

    class Meta:
        managed = False
        db_table = 'card'


class Permissions(models.Model):
    person = models.ForeignKey(Person, models.DO_NOTHING)
    card = models.ForeignKey(Card, models.DO_NOTHING)
    owner = models.BooleanField()

    class Meta:
        managed = False
        db_table = 'permissions'
        unique_together = (('person', 'card'),)

Reading through Django documentation ( https://docs.djangoproject.com/en/3.2/topics/db/models/#intermediary-manytomany ) regarding this exact problem, they don’t seem to provide the solution to this, but rather just mention using multiple different queries using previous query results (at least how I understood the solution from them), which seems really expensive for such a simple query.

Asked By: Nuubles

||

Answers:

You can filter with:

from django.db.models import F

Card.objects.annotate(owner=F('permissions__owner')).filter(
    permissions__person_id=some_id
)

The Card objects from this queryset will have an extra attribute .owner that will specify if that person is indeed the owner.


Note: normally a Django model is given a singular name, so Permission instead of Permissions.


Note: Your Permissions model acts as a junction table for a many-to-many relation between Card and Person. You can span a
ManyToManyField [Django-doc]
on the Card model with:

class Card(models.Model):
    # …
    persons = models.ManyToManyField(
        Person,
        through='Permissions'
    )
Answered By: Willem Van Onsem
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.