I have a project with these models and relations:
- Player
- Team
- Games
- Coach
- Doctor
- MedicalSpecialty
- Sponsor
- Team
This is an example, of course, but as you can see I can have on table Player
a team_id
column as FK (Foreign Key) for Team
which has coach_id
column as FK for Coach
table.
In my Golang backend I'm using an ORM that allows you to (generate and) use this kind of code:
type Player struct {
id string
name string
Team *Team
//...
}
type Team struct {
id string
Coach *Coach
//...
}
type Coach struct {
id string
//...
}
func (repo *Repo) PlayerById(id string) DomainPlayer {
player := repo.queryById(id).withTeam().withCoach()
// player here has TEAM and TEAM.COACH using a unique SQL query
// and I can easily convert it:
return ConvertPlayerToDomainPlayer(player)
}
Starting with Rust I was fascinated by SeaORM but now that I'm trying to create something more complex I have realized that I cannot do things like in Go.
I'm generating entities using sea-orm-cli
from my Postgresql DB created using SeaORM Migrator (this is totally optional, you can also write the following code by yourself).
The generated entities are like this:
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "player")]
pub struct Model {
#[sea_orm(primary_key, unique)]
pub id: String,
pub name: String,
pub team_id: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::team::Entity",
from = "Column::TeamId",
to = "super::team::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Team,
}
impl Related<super::team::Entity> for Entity {
fn to() -> RelationDef {
Relation::Team.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
And I'm using code like:
async fn player_by_id(&self, id: String) -> Result<Option<DomainPlayer>> {
let player = match Player::find_by_id(id)
.find_also_related(Team)
// I would like to use here something like `with_team()` and `with_coach()` or `with_team(|team| team.with_coach())`
.one(self.db)
.await?
{
Some(o) => o,
None => return Ok(None),
};
let coach = if let Some(team) = &player.1 {
Coach::find_by_id(team.coach_id).one(self.db).await?
} else {
None
};
// Since I need to return a DomainPlayer (not a Player) here I cannot use a `Player.into()` because generated `Player` does not have `Team` field
Ok(Some(custom_func_to_transform_player_to_domain_player(player, player.1 /*this is team*/, coach)))
// Do you can imagine how complex is to use these functions?
}
I'd like to know what you think of this mental order.
Most likely I need to change it because the code is really ugly (as well as expensive for the many SQL queries and allocations) and inconvenient to use to scale with even more complexity.
What do you recommend?