Questions about more complex scenarios using SeaORM and clean architecture

I have a project with these models and relations:

  1. Player
    1. Team
      1. Games
      2. Coach
      3. Doctor
        1. MedicalSpecialty
    2. Sponsor

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?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.