Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Questions about more complex scenarios using SeaORM and clean architecture #15

Open
frederikhors opened this issue Oct 21, 2022 · 7 comments

Comments

@frederikhors
Copy link

@bkonkle I saw that you answered "on video" to the other two issues and soon I will be able to watch and learn from the wonder you are creating.

Since you are a content creator perhaps the following question could offer you more food for thought and therefore videos! : smile:


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?

@bkonkle
Copy link
Owner

bkonkle commented Oct 21, 2022

Thank you! I do indeed love ideas for new content, so I appreciate you taking the time to put together so much detail! I've definitely struggled a bit with this aspect of SeaORM, and even went so far as to fork it to support more JOINs. I came up with some good strategies, but sometimes you'll just have to fall back to something more like raw SQL, unfortunately. I'll take some time to review this over the weekend and come up with a good approach.

@bkonkle
Copy link
Owner

bkonkle commented Nov 15, 2022

I haven't found an approach I really like yet. In my own projects, I've typically been using Dataloaders to work around this. I'll optimize one relationship with find_also_related, and then batch-load any other relationships with a dataloader. This definitely isn't as efficient as JOINS, but it works. I believe this is the approach taken by their seaography library, which they mention in their "cookbook" entry about this problem.

I did, however, find another page in their cookbook that mentions using the SeaQuery builder and converting it into your entity type using FromQueryResult, which SeaORM can derive for you. I've used this method for custom joins in other projects, but next time I think I'll use SeaQuery instead. I like the API better.

I'll try to find the time soon to add an example here - maybe using a custom join to list Shows, join in Episodes, and also join in Users who have a RoleGrant for the "guest" role for each Episode related to each Show. If I can find a way to use FromQueryResult to transform that to my internal Rust structs, then we might have a way to handle most use cases.

@frederikhors
Copy link
Author

You've described pretty much everything I've found and tried too.

I'm thinking of trying directly with https://github.com/launchbadge/sqlx.

The example you mention would be very handy also because there are only "hello world" examples around.

@frederikhors
Copy link
Author

I also opened dpc/sniper#3. I suggest you to read because there is another idea to use a "Store" which is an HashMap to carry around with the data inserted and transformed only once without using Box or taking up much space on the stack using struct fields. What do you think?

@frederikhors
Copy link
Author

Did you find a better way?

@frederikhors
Copy link
Author

@bkonkle
Copy link
Owner

bkonkle commented Feb 10, 2023

Sadly, I've been at full capacity and haven't been able to come back to this. I'm working on a big Go project, and also trying to build up a Rust side-project at the same time, while also trying to extract a framework that I can use to build more side projects faster. 😅 I'm planning on coming back to this as I implement an example project in my new framework, but I just haven't had the time to do so yet. Sorry for the slow response here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants