Describe the bug
If using select_related, it raises AttributeError
Seems like the error is related to one-to-many relation
To Reproduce
short:
# query:
t: Tournament = await Tournament.filter(name=tournament_name).select_related("events").first()
# exception
File "/opt/project/api/stuff.py", line 51, in run
t: Tournament = await Tournament.filter(name=tournament_name).select_related("events").first()
โ โ โ โ 'tournament'
โ โ โ <classmethod object at 0x7f95e62e5df0>
โ โ <class '__main__.Tournament'>
โ <Tournament: 1>
File "/usr/local/lib/python3.8/site-packages/tortoise/queryset.py", line 797, in _execute
instance_list = await self._db.executor_class(
โ โ <member '_db' of 'QuerySet' objects>
โ <tortoise.queryset.QuerySet object at 0x7f95e5349c70>
File "/usr/local/lib/python3.8/site-packages/tortoise/backends/base/executor.py", line 149, in execute_select
setattr(ins, model_name, obj)
โ โ โ <Event: 1>
โ โ 'events'
โ <Tournament: 1>
AttributeError: can't set attribute
Expected behavior
Don't raise, just load selected relations
Additional context
from tortoise import Tortoise, fields, run_async
from tortoise.models import Model
class Tournament(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
events: fields.ReverseRelation["Event"]
def __str__(self):
return self.name
class Event(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament",
related_name="events",
)
def __str__(self):
return self.name
async def run():
await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
await Tortoise.generate_schemas()
tournament_name = "tournament"
tournament = await Tournament.create(name=tournament_name)
await Event.create(name="First", tournament=tournament)
await Event.create(name="Second", tournament=tournament)
t: Tournament = await Tournament.filter(name=tournament_name).first()
print(t, t.events)
# tournament <tortoise.fields.relational.ReverseRelation object at 0x7f7e4b693e50>
t: Tournament = await Tournament.filter(name=tournament_name).prefetch_related("events").first()
print(t, t.events, list(t.events))
# tournament <tortoise.fields.relational.ReverseRelation object at 0x7f7e4b6b3760> [<Event: 1>, <Event: 2>]
t: Tournament = await Tournament.filter(name=tournament_name).select_related("events").first()
print(t, t.events, list(t.events))
# Traceback (most recent call last):
# File "/opt/project/api/stuff.py", line 47, in <module>
# run_async(run())
# File "/usr/local/lib/python3.8/site-packages/tortoise/__init__.py", line 636, in run_async
# loop.run_until_complete(coro)
# File "/usr/local/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
# return future.result()
# File "/opt/project/api/stuff.py", line 42, in run
# t: Tournament = await Tournament.filter(name=tournament_name).select_related("events").first()
# File "/usr/local/lib/python3.8/site-packages/tortoise/queryset.py", line 797, in _execute
# instance_list = await self._db.executor_class(
# File "/usr/local/lib/python3.8/site-packages/tortoise/backends/base/executor.py", line 149, in execute_select
# setattr(ins, model_name, obj)
# AttributeError: can't set attribute
if __name__ == "__main__":
run_async(run())
relates to #500 and #501
select_related only work in ForeignKeyRelation, not work in ReverseRelation. And select_related generate sql join, which has different behavior with prefetch_related
Anyway there has to be the way to select related items in one query. It's strange that there's no option to do this join yet
@mahenzon but FK relation is "One to Many". There is no way to write such a query even in raw SQL, to join all related items, which has a link to an object. Only some aggregations, like array_agg, but this is not a join at all, it's just a sub-query.
@DrJackilD I'm sorry, I don't understand your point. What's wrong with one-to-many relationship?
There're two ways to do it:
1 - in two queries. at first query main objects, then collect their ids and then fetch related objects
2 - in one query, using join.
anyways in both cases we'll have to create python objects and connect them together
Creating tables and a relation with foreign key:
CREATE TABLE authors (
author_id serial PRIMARY KEY,
username VARCHAR ( 50 ) UNIQUE NOT NULL
);
CREATE TABLE articles (
article_id serial PRIMARY KEY,
title VARCHAR ( 50 ) NOT NULL,
body text NOT NULL,
author_id INT NOT NULL,
CONSTRAINT fk_author
FOREIGN KEY(author_id)
REFERENCES authors(author_id)
);
INSERT INTO authors (username) VALUES ('john');
INSERT INTO authors (username) VALUES ('james');
INSERT INTO authors (username) VALUES ('sam');
INSERT INTO articles (title, body, author_id) VALUES ('python lesson', 'text body', 1);
INSERT INTO articles (title, body, author_id) VALUES ('tortoise lesson', 'another body', 1);
INSERT INTO articles (title, body, author_id) VALUES ('django lesson', 'some body text', 2);
Queries:
-- selecting author by article.author_id ( =1)
SELECT author_id, username
FROM authors
WHERE author_id = 1;
| author_id | username |
| :--- | :--- |
| 1 | john |
-- selecting all authored posts by author.author_id
SELECT article_id, title, body, author_id
FROM articles
WHERE articles.author_id = 1;
| article_id | title | body | author_id |
| :--- | :--- | :--- | :--- |
| 1 | python lesson | text body | 1 |
| 2 | tortoise lesson | another body | 1 |
SELECT au.author_id, au.username, ar.article_id, ar.title, ar.body
FROM authors au
LEFT OUTER JOIN articles ar on au.author_id = ar.author_id
WHERE username ILIKE 'j%';
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 1 | john | 1 | python lesson | text body |
| 1 | john | 2 | tortoise lesson | another body |
| 2 | james | 3 | django lesson | some body text |
-- selecting one selected author's articles
SELECT au.author_id, au.username, ar.article_id, ar.title, ar.body
FROM authors au
LEFT OUTER JOIN articles ar on au.author_id = ar.author_id
WHERE username = 'james';
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 2 | james | 3 | django lesson | some body text |
-- selecting some selected authors articles
SELECT au.author_id, au.username, ar.article_id, ar.title, ar.body
FROM authors au
LEFT OUTER JOIN articles ar on au.author_id = ar.author_id
WHERE username ILIKE '%a%';
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 2 | james | 3 | django lesson | some body text |
| 3 | sam | NULL | NULL | NULL |
-- all authors with their articles
SELECT au.author_id, au.username, ar.article_id, ar.title, ar.body
FROM authors au
LEFT OUTER JOIN articles ar on au.author_id = ar.author_id;
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 1 | john | 1 | python lesson | text body |
| 1 | john | 2 | tortoise lesson | another body |
| 2 | james | 3 | django lesson | some body text |
| 3 | sam | NULL | NULL | NULL |
No offence, but...
In django orm it just works, but here is not.
Also, AttributeError: can't set attribute is a very strange error.
It can be more specific, like You cannot do what you're used to, just use "prefetch_related" instead
=) Just kidding.
Any news?
Most helpful comment
@DrJackilD I'm sorry, I don't understand your point. What's wrong with one-to-many relationship?
There're two ways to do it:
1 - in two queries. at first query main objects, then collect their ids and then fetch related objects
2 - in one query, using join.
anyways in both cases we'll have to create python objects and connect them together
Creating tables and a relation with foreign key:
Queries:
| author_id | username |
| :--- | :--- |
| 1 | john |
| article_id | title | body | author_id |
| :--- | :--- | :--- | :--- |
| 1 | python lesson | text body | 1 |
| 2 | tortoise lesson | another body | 1 |
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 1 | john | 1 | python lesson | text body |
| 1 | john | 2 | tortoise lesson | another body |
| 2 | james | 3 | django lesson | some body text |
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 2 | james | 3 | django lesson | some body text |
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 2 | james | 3 | django lesson | some body text |
| 3 | sam | NULL | NULL | NULL |
| author_id | username | article_id | title | body |
| :--- | :--- | :--- | :--- | :--- |
| 1 | john | 1 | python lesson | text body |
| 1 | john | 2 | tortoise lesson | another body |
| 2 | james | 3 | django lesson | some body text |
| 3 | sam | NULL | NULL | NULL |