sea-orm icon indicating copy to clipboard operation
sea-orm copied to clipboard

Joins from aliased tables: add a helper method or document my workaround

Open Expurple opened this issue 2 years ago • 0 comments

My simplified use case

Let's define a country table, a city table that references it, and a trip table that references two cities: the source and the destination. Then, for a given trip.id, I want to select source_country_name and destination_country_name in a single ergonomic query. This can be achieved by using two parallel join chains, with table aliases added for disambiguation.

For quite a while, I was stuck trying to compile this:

use sea_orm::JoinType::LeftJoin;
use sea_orm::*;
use sea_query::*;

// There are also equivalent aliases for the destination.
let source = Alias::new("source");
let source_country = Alias::new("source_country");

trip::Entity::find()
  .select_only()  
  // There are also equivalent method calls for the destination. They use City2 instead of City1.
  .column_as(Expr::col((source_country.clone(), country::Column::Name)), "source_country_name")
  .join_as(LeftJoin, trip::Relation::City1.def(), source)
  .join_as(LeftJoin, /* ??? relation from source.country_id to country ??? */, source_country)
  
  // Other irrelevant method calls are omitted.

My current workaround

Eventually, I figured out that I can define

let source_to_country_relation = {
  let mut rel = city::Relation::Country.def();
  rel.from_tbl = rel.from_tbl.alias(source.clone());
  rel
};

and fill the hole with that:

  .join_as(LeftJoin, source_to_country_relation, source_country)

Proposed solutions

Please add and document some hepler method that can be quicky discovered via IDE autocompletion or docs and express my intent concisely:

  .join_as(LeftJoin, city::Relation::Country.def().from_alias(source), source_country)
  
  // or
  
  .join_from_as(LeftJoin, source, city::Relation::Country.def(), source_country)
  
  // or something similar

Once we've set on the desired API, maybe I can even implement it, this seems to be easy. If we see that the implementation will take a while or won't happen at all, let's at least mention my workaround in the docs.

On a side note, I'd really appreciate if I could achieve the same thing with find_also_related()/find_also_linked() instead of join()/join_as(). That's really the idiomatic sea-orm way of doing joins, IMO. And it's less verbose, especially find_also_linked(). But seems like we need basic #1028 first, before talking about using aliases there.

Approximate migration code for playing around

use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
  async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    manager
      .create_table(
        Table::create()
          .table(Country::Table)
          .col(ColumnDef::new(Country::Id).integer().not_null().auto_increment().primary_key())
          .col(ColumnDef::new(Country::Name).string().not_null())
          .to_owned(),
      )
      .await?;
    manager
      .create_table(
        Table::create()
          .table(City::Table)
          .col(ColumnDef::new(City::Id).integer().not_null().auto_increment().primary_key())
          .col(ColumnDef::new(City::CountryId).integer().not_null())
          .foreign_key(
            ForeignKey::create()
              .from(City::Table, City::CountryId)
              .to(Country::Table, Country::Id),
          )
          .to_owned(),
      )
      .await?;
    manager
      .create_table(
        Table::create()
          .table(Trip::Table)
          .col(ColumnDef::new(Trip::Id).integer().not_null().auto_increment().primary_key())
          .col(ColumnDef::new(Trip::SourceId).integer().not_null())
          .col(ColumnDef::new(Trip::DestinationId).integer().not_null())
          .foreign_key(
            ForeignKey::create()
              .from(Trip::Table, Trip::SourceId)
              .to(City::Table, City::Id),
          )
          .foreign_key(
            ForeignKey::create()
              .from(Trip::Table, Trip::DestinationId)
              .to(City::Table, City::Id),
          )
          .to_owned(),
      )
      .await
  }

  async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> {
    unimplemented!();
  }
}

#[derive(DeriveIden)]
pub enum Country {
  Table,
  Id,
  Name,
}

#[derive(DeriveIden)]
pub enum City {
  Table,
  Id,
  CountryId,
}

#[derive(DeriveIden)]
pub enum Trip {
  Table,
  Id,
  SourceId,
  DestinationId,
}

Expurple avatar Nov 20 '23 19:11 Expurple