Skip to content

Associations: what should they look like? #89

@sgrif

Description

@sgrif

Let's talk about associations. First what's in the public API today, what's in the private API today, and what they actually need to do. We can use those things to drive what the API should look like.

Public API today

Right now Table has inner_join and left_outer_join which can take any other table which implements JoinTo. This is limited to exactly one table today, due to some limitations that I think will be resolved by specialization. There also needs to be some boilerplate implementations of SelectableExpression for all the various types of query sources, which will go away when specialization (or rust-lang/rust#29864) lands and replaced with:

impl<Left, Right, ST, T> SelectableExpression<InnerJoinSource<Left, Right>, ST>
    for T where
    T: SelectableExpression<Left, ST>,
    Left: JoinTo<Right>,
{}

impl<Left, Right, ST, T> SelectableExpression<InnerJoinSource<Left, Right>, ST>
    for T where
    T: SelectableExpression<Right, ST>,
    Left: JoinTo<Right>,
{}

This will also basically allow us to implement the join methods on the various join sources as well, though we might need to do some tuple hackery.

Private API today

The annotations #[has_many] and #[belongs_to] are part of the internal API today. Both implement JoinTo automatically. belongs_to additionally implements BelongingToDsl<Parent> for the child. I wrote up some thoughts on where that is today at #86 (comment).

What do Associations actually need to do?

Ultimately we're either eager loading the children for a collection of parents, or we're loading the children for a single parent. I believe that BelongingTo sufficiently handles the latter, but it'll effectively be handled by the former.

One To Many

I'm pretty reasonably confident that associations should be non invasive (user doesn't know it has many posts). That means that the type of a user and all of its posts is (User, Vec<Post>). This can get tricky when dealing with multiple levels of nesting here. For example, the type of a user, all the posts they've written, and all the comments written by that user is (User, Vec<Post>, Vec<Comment>). By contrast, the type of a user, all of the posts they've written, and all of the comments left on each of those posts would be (User, Vec<(Post, Vec<Comment>)>).

I'd imagine the way to specify that you want to load the comments for the posts and not for the users is by writing users.left_outer_join(posts.left_outer_join(comments)) as opposed to users.left_outer_join(posts).left_outer_join(comments).

The case of a user and all of its posts would be written today as:

users.left_outer_joins(posts::table).load(&connection)
    .group_by(|(user, post)| user)
    .map(|(user, posts)| posts.into_iter().filter_map(|p| p).collect())
    .collect()

The case involving comments is effectively impossible today.

We do not need to have specific code to handle loading the children for a single record, as Post::belonging_to(&user) is sufficient.

It should also be noted that I don't want to restrict what you're able to work with. We should be able to get a user, and the title of all of their posts by doing users.left_outer_joins(posts::table).select((users.all_columns(), posts::title)).

One To One

The type of a one to one relationship is either (A, B) or (A, Option<B>). Technically only a belongs_to can be mandatory, but you can still end up with the first signature with an inner join. Regardless of if we're going child to parent or parent to child, the way this is written today is simply:

users.inner_join(profile)

And loading a single record is handled by Post::belonging_to(&user). I do not believe we need any additional code to handle this case, but we could potentially rename the join methods to have parity with whatever we come up with for one to many.

Composition

Associations should be composable. If we want to get all of the comments that have been written for any posts written by a user, we should be able to re-use our existing logic, without having to actually load the posts.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions