How to access a parent model's relationship's attributes #1928
-
First Check
Commit to Help
Example Codeclass UserBase(Base):
username: str
in_game_name: str
discord_name: Optional[str] = Field(default=None)
is_active: Optional[bool] = Field(default=True)
is_superuser: Optional[bool] = Field(default=False)
company: Optional[CompanyUser] = Relationship(back_populates="company")
class UserRead(UserBase):
rank: str = UserBase.company.rank
---------
class CompanyUser(SQLModel, table=True):
"""
Link Table to store ranks between users and a company
"""
company_id: uuid.UUID = Field(foreign_key="company.id", primary_key=True)
user_id: uuid.UUID = Field(foreign_key="user.id", primary_key=True)
rank: str
company: "CompanyBase" = Relationship(back_populates="members")
user: "UserBase" = Relationship(back_populates="company")
class CompanyBase(Base):
name: str
logo_id: Optional[uuid.UUID] = Field(default=None, foreign_key="file.id")
members: List[CompanyUser] = Relationship(back_populates="user")DescriptionErroring on UserRead>rank: UserBase has no attribute "company". Effectively, I'm unsure how to access the parent model's relationships. Operating SystemLinux, Windows Operating System DetailsNo response SQLModel Version0.0.4 Python Version3.9.7 Additional ContextTrying to follow this guide on link tables with attributes: https://sqlmodel.tiangolo.com/tutorial/many-to-many/link-with-extra-fields/ |
Beta Was this translation helpful? Give feedback.
Replies: 14 comments
-
|
UserBase.company isn't a class constant so you won't be able to access it like that. As for the SQLModel part, the docs mention not to inherit tables (which I'm assuming UserBase is here since I'm not sure what If |
Beta Was this translation helpful? Give feedback.
-
|
UserBase is not a table. Neither is Base. Only User has the table=True. It's defintion was essentially inheriting from UserBase with a table=True and passing on the body. It just seems weird that I have to redefine columns over and over again for each function (read, create, etc) rather than having a base and inheriting. I tried using this from some other gh issues: but that also doesnt populate in the return object |
Beta Was this translation helpful? Give feedback.
-
|
Ok, so you'd want (and potentially need) to have the relationship on the table not the data model, so At least, this is what I've done in the past for this. In your case I believe it would look something like this, but I'd need your full example to be more specific: class UserRead(UserBase):
company: str = Field(..., alias="rank")
@validator("company", pre=True)
def get_rank(cls, company: CompanyUser) -> str:
return company.rank
class Config:
allow_popoulation_by_field_name: True
class User(UserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
company: Optional[CompanyUser] = Relationship(back_populates="company")I'd like to add though that I've not confirmed this works on SQLModel, I believe it should though. Not sure if there is a better way or not. |
Beta Was this translation helpful? Give feedback.
-
|
I guess I'm just lost on the how. The docs say that you should make a base data class and have your actual table inherit from that, and never have things inherit from your table. but if that's true then how do you setup a read model to get relationships if they cant see the relationships?? further, especially with a user, only the table model will have the password (intended), but the read model can literally never see the blog posts associated with a user, for example, since that is in a parallel object. Because when the docs talk about relationships, they show it on the base object (not table) and have read's inherit from that |
Beta Was this translation helpful? Give feedback.
-
class UserBase(SQLModel):
name: str
class UserRead(UserBase):
company: str = Field(..., alias="rank")
@validator("company", pre=True)
def get_rank(cls, company: CompanyUser) -> str:
return company.rank
class Config:
allow_popoulation_by_field_name: True
class User(UserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
password: str
company: Optional[CompanyUser] = Relationship(back_populates="company")
@app.get("/user/{id}", response_model=UserRead)
def get_user(/* db connection */, id: str):
...
...
return /* some object of type User */Assuming you are using SQLModel with FastAPI, when you return data you specify which model you want that data to fit into. In this case, if you get a You can replace
Where in the docs is this? I only see relationships being put on tables not data classes (since as I said it probably won't work at all on data classes). |
Beta Was this translation helpful? Give feedback.
-
|
Stumbled on to this comment and attempted to get it working and was struggling. There were two issues with the sample code, both on the That should work. With the code as is, the config wasn't taking and it required |
Beta Was this translation helpful? Give feedback.
-
|
While this example works just fine if you're attempting to access a single attribute on the parent, it breaks down when you need to access two different attributes. Using the examples from the documentation: If I wanted the It's not clear how to define the The above clearly won't work because I have two attributes called |
Beta Was this translation helpful? Give feedback.
-
|
So this starts to get kind of janky, but you can get class HeroRead(HeroBase):
id: int
team_name: Optional[str] = None # default value matters here
team: int = Field(alias="team_id")
@validator("team", pre=True)
def get_team_id(cls, team: TeamRead, values: Dict) -> int:
values['team_name'] = team.name
return team.id
class Config:
allow_population_by_field_name = TrueFrom the pydantic docs, by adding There is also the option of nesting a |
Beta Was this translation helpful? Give feedback.
-
|
Got it, that all makes sense. Agreed it feels janky and a perversion of validators. Inherently, validators are used to "validate" data but really we're using them to mutate / transform here. It's not inherently bad, just a misnomer. Also agreed that a better option is to nest the Long term, SQLModel should have a better way to support this type of behavior but this feels like an acceptable workaround for now. |
Beta Was this translation helpful? Give feedback.
-
|
I've tried to implement the above solution and I've got: I've tried to add I'm new to sqlmodel and fastapi and I was surprised that this use case is so difficult to implement. Any advice to fix at least my error for the time being? |
Beta Was this translation helpful? Give feedback.
-
You need to pass |
Beta Was this translation helpful? Give feedback.
-
|
@lovetoburnswhen I solved thanks to your comment 👍 I will publish my case so it might maybe help someone. And back in fastAPI @app.get("/parents", response_model=List[MyParentModelRead])
def read_parents(offset: int = 0, limit: int = Query(default=100, lte=100)):
with Session(engine) as session:
parents = session.exec(select(MyParentModel).offset(offset).limit(limit)).all()
return parentsThis will correctly returns: Still, I think using validation to do this feels 'hackish' 🤷♂️ |
Beta Was this translation helpful? Give feedback.
-
@lovetoburnswhen btw I couldn't find the |
Beta Was this translation helpful? Give feedback.
-
|
I think the most straightforward solution here would be to mimic structure of DB models in Read models: create the class UserBase(SQLModel):
username: str
class CompanyUserRead(SQLModel):
rank: str
class UserRead(UserBase):
company: CompanyUserReadAnd then add a computed field to provide direct access to the class UserRead(UserBase):
company: CompanyUserRead
@computed_field
@property
def rank(self) -> str:
return self.company.rankYou can also exclude company: CompanyUserRead = Field(exclude=True)Runnable code example in the details: Detailsimport uuid
from typing import List, Optional
from pydantic import computed_field
from sqlalchemy.orm import selectinload
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
class UserBase(SQLModel):
username: str
class User(UserBase, table=True):
id: uuid.UUID = Field(primary_key=True)
company: Optional["CompanyUser"] = Relationship(back_populates="user")
class CompanyUserRead(SQLModel):
rank: str
class UserRead(UserBase):
company: CompanyUserRead = Field(exclude=True)
@computed_field
@property
def rank(self) -> str:
return self.company.rank
class Company(SQLModel, table=True):
id: uuid.UUID = Field(primary_key=True)
name: str
members: List["CompanyUser"] = Relationship(back_populates="company")
class CompanyUser(SQLModel, table=True):
"""
Link Table to store ranks between users and a company
"""
company_id: uuid.UUID = Field(foreign_key="company.id", primary_key=True)
user_id: uuid.UUID = Field(foreign_key="user.id", primary_key=True)
rank: str
company: "Company" = Relationship(back_populates="members")
user: "User" = Relationship(back_populates="company")
engine = create_engine("sqlite:///")
user_id = uuid.uuid4()
def init_db():
SQLModel.metadata.create_all(engine)
# Add data to DB
with Session(engine) as session:
company = Company(id=uuid.uuid4(), name="Company 1")
user = User(id=user_id, username="user 1")
company_user = CompanyUser(user=user, company=company, rank="123")
session.add(company_user)
session.commit()
def main():
init_db()
# Read User from DB
with Session(engine) as session:
user_db = session.get(User, user_id, options=[selectinload(User.company)])
user_read = UserRead.model_validate(user_db)
assert user_read.rank == "123"
assert user_read.company.rank == "123"
assert user_read.model_dump() == {"username": "user 1", "rank": "123"}
# You can also validate UserRead from dict
user_from_dict = UserRead.model_validate(
{"username": "user 1", "company": {"rank": "123"}}
)
assert user_from_dict.rank == "123"
assert user_from_dict.model_dump() == {"username": "user 1", "rank": "123"}
if __name__ == "__main__":
main()So, you will be able to access If you need to implement direct access from As for treating this as feature request, I'm not sure this feature is so valuable - use case seems to be quite niche and there is already a way to implement this.. |
Beta Was this translation helpful? Give feedback.
I think the most straightforward solution here would be to mimic structure of DB models in Read models: create the
CompanyUserReadschema with therankfield and add it toUserReadschema.And then add a computed field to provide direct access to the
rankfield:You can also exclude
companyfield from output schema:Runnable code example in the d…