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.
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'
)
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.
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 betweenCard
andPerson
. You can span a
ManyToManyField
[Django-doc]
on theCard
model with:class Card(models.Model): # … persons = models.ManyToManyField( Person, through='Permissions' )