juniper-eager-loading icon indicating copy to clipboard operation
juniper-eager-loading copied to clipboard

HasManyThrough with other fields

Open Farkal opened this issue 5 years ago • 5 comments

How can i join two tables and add other attributes from the join table ?

I have the following database schema image

My graphql scheme look like this:

schema {
  query: Query
  mutation: Mutation
}

type Query {
  cinemas(
    criterias: CinemaSearchCriterias
    pagination: Pagination = { perPage: 20 }
    order: Order = "-id"
  ): [Cinema]!
  movies(
    criterias: MovieSearchCriterias
    pagination: Pagination = { perPage: 20 }
    order: Order = "-id"
  ): [Movie]!
}

type Mutation {
}

scalar Order

enum EExposedFormat {
  FULLHD
  4K
  3D
  4D
}

type Cinema {
  id: ID!
  name: String!
  movies(
    criterias: MovieSearchCriterias
    pagination: Pagination = { perPage: 20 }
    order: Order = "-id"
  ): [PlannedMovie]
}

type PlannedMovie {
  id: ID!
  exposedFormat: EExposedFormat
  boundingBox: [Float]
  movie: Movie!
}

type Movie {
  id: ID!
  name: String!
  description: String
  pixelsBox: [Float]
  path: String
}

input Pagination {
  page: Int
  perPage: Int!
}

input CinemaSearchCriterias {
  name: String
}

input MovieSearchCriterias {
  name: String
  path: String
}

So when i request the cinema i need to get the PlannedMovie type for each movie, how can i create that object using juniper eager loading (it doesn't exist in db so i don't know witch model i am supposed to put when implementing EagerLoading)

Also i think we don't have complete example of complex use case. I have the following database scheme https://github.com/Farkal/test-wundergraph/blob/master/test_data_model_v1.png (i tried to implement it using wundergraph but there is a rustc issue with repetitive trait bound that make long compilation time). I think it could be a great and complete example of complex use case for juniper-eager-loading and for graphql api using rust in general. If you could help me implementing some part that would be awesome ! (i plan to create the graphql scheme in few hours)

Farkal avatar Feb 04 '20 14:02 Farkal

juniper-eager-loading doesn't require that your structs map directly to your database schema. So you're fully able to make structs that compose several database models. Doing that will require implementing EagerLoadChildrenOfType manually though. You're free to decide if load_children should use the LoadFrom trait to make the queries or just make them directly in load_children. When doing manual implementations that isn't important. The rest of the methods in EagerLoadChildrenOfType should be fairly straight forward.

You can find an example of how to do that here and one here if your GraphQL fields need to take arguments that impact eager loading.

I have done something like in the past so it is totally possible but it does require wrapping your head around the concepts. Give it a shot and let me know if you run into any problems.

davidpdrsn avatar Feb 04 '20 14:02 davidpdrsn

Waow that was quick ! Thank's for the information I will try to do something :+1:

Farkal avatar Feb 04 '20 14:02 Farkal

Sorry but I don't find how to create custom type from HasManyThrough. I am able to link Cinema to its Movies through the MovieToCinema model, but impossible to find how return custom type that contains layer information.

Here is my current code:

#[derive(Clone, EagerLoading)]
#[eager_loading(context= Context, error = Error)]
pub struct Cinema {
    cinema: models::Cinema,

    #[has_many_through(join_model = models::MovieToCinema)]
    movies: HasManyThrough<PlannedMovie>,
}

impl CinemaFields for Cinema {
    fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<ID> {
        Ok(self.cinema.id.to_string().into())
    }

    fn field_name(&self, executor: &Executor<'_, Context>) -> FieldResult<&String> {
        Ok(&self.cinema.name)
    }

    fn field_movies(
        &self,
        executor: &Executor<'_, Context>,
        trail: &QueryTrail<'_, PlannedMovie, Walked>,
        criterias: Option<MovieSearchCriterias>,
        pagination: Option<Pagination>,
        order: Option<Order>,
    ) -> FieldResult<&Option<Vec<PlannedMovie>>> {
        self.movies.try_unwrap().map_err(From::from)
    }
}

impl juniper_eager_loading::LoadFrom<models::Cinema> for models::MovieToCinema {
    type Error = Error;
    type Context = Context;

    fn load(
        cinemas: &[models::Cinema],
        _field_args: &(),
        ctx: &Self::Context,
    ) -> TRustResult<Vec<Self>> {
        let cinema_ids = cinemas.iter().map(|e| e.id).collect::<Vec<_>>();

        movie_to_cinema::get_by_cinemas_ids(&ctx.db, &cinema_ids)
    }
}

impl juniper_eager_loading::LoadFrom<models::MovieToCinema> for Movie {
    type Error = Error;
    type Context = Context;

    fn load(
        movies_to_cinemas: &[models::MovieToCinema],
        _field_args: &(),
        ctx: &Self::Context,
    ) -> TRustResult<Vec<Self>> {
        let movies_ids = movies_to_cinemas
            .iter()
            .map(|movie_to_cinema| movie_to_cinema.movie_id)
            .collect::<Vec<_>>();

        movie::get_by_ids(&ctx.db, &movies_ids)
    }
}

struct EagerLoadingContextMovieToCinema;

impl<'a>
    EagerLoadChildrenOfType<
        'a,
        PlannedMovie,
        EagerLoadingContextMovieToCinema,
        models::MovieToCinema,
    > for Cinema
{
    type FieldArguments = ();

    fn load_children(
        models: &[Self::Model],
        field_args: &Self::FieldArguments,
        ctx: &Self::Context,
    ) -> Result<LoadChildrenOutput<PlannedMovie, models::MovieToCinema>, Self::Error> {
        let join_models: Vec<models::MovieToCinema> = LoadFrom::load(&models, field_args, ctx)?;
        let child_models: Vec<models::Movie> = LoadFrom::load(&join_models, field_args, ctx)?;

        let mut child_and_join_model_pairs = Vec::new();

        // // WANT TO CONVERT MOVIE TO PLANNED MOVIE BY MERGING WITH MOVIETOCINEMA HERE BUT CAN'T BECAUSE RETURN TYPE NEED TO IMPL EAGERLOADING

        Ok(LoadChildrenOutput::ChildAndJoinModels(
            child_and_join_model_pairs,
        ))
    }

    fn is_child_of(
        node: &Self,
        child: &Movie,
        join_model: &models::MovieToCinema,
        _field_args: &Self::FieldArguments,
        _ctx: &Self::Context,
    ) -> bool {
        node.cinema.id == join_model.cinema_id && join_model.movie_id == child.movie.id
    }

    fn association(node: &mut Self) -> &mut dyn Association<Movie> {
        &mut node.movies
    }
}

My other solution is to don't use the HasManyThrough but create a HasMany from Cinema to PlannedMovie and a HasOne from PlannedMovie to Movie. It should work and maybe it's more simpler (i don't know about the perf)

Farkal avatar Feb 05 '20 14:02 Farkal

Are you able to share all your code so I can poke around? A repo I could fork/clone would be ideal.

davidpdrsn avatar Feb 05 '20 15:02 davidpdrsn

Yes ! Here -> https://github.com/Farkal/rust-graphql-complex-api-example

Could you also release the latest version of the lib supporting field_arguments ? (it seems it doesn't support Option<MyFieldArg> i will implement EagerLoadChildrenOfType and investigate on this)

Farkal avatar Feb 06 '20 15:02 Farkal