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

Polymorphic Associations #58

Closed
CanRau opened this issue Oct 29, 2023 · 9 comments
Closed

Polymorphic Associations #58

CanRau opened this issue Oct 29, 2023 · 9 comments
Labels

Comments

@CanRau
Copy link

CanRau commented Oct 29, 2023

Hey, very interesting project 🙌

Curious if polymorphism is already possible or planned?

E.g. I have a table commenting which has commentable_type & commentable_id fields where type could be article and id the articles id

I'm currently finally evaluating an ORM for our project as things get a little unwieldy using mysql2 directly writing manual types & queries 🥴

Edit: maybe this discussion on polymorphic associations could be interesting drizzle-team/drizzle-orm#1051

@lroal lroal added the invalid label Oct 30, 2023
@lroal
Copy link
Member

lroal commented Oct 30, 2023

Hello, thank's for your interest in RDB.
It would not make sense to add polymorphism unless there is a way to add custom logic to the row. Today, this is not possible.
I think this would be solved by the implementing hooks #60 instead , so you can add custom logic/class based on the type of the row.
So am closing this issue as it is redundant when #60 is implemented.

@lroal lroal closed this as completed Oct 30, 2023
@lroal lroal added duplicate and removed invalid labels Oct 30, 2023
@lroal
Copy link
Member

lroal commented Oct 30, 2023

Feel free to give some feedback, if i misinterpreted anything or didn't get the essence of your question.
Maybe you can elaborate with a more detailed example ?

@lroal
Copy link
Member

lroal commented Oct 30, 2023

Reopening it, so @CanRau can elaborate with example and use case first.

@lroal lroal reopened this Oct 30, 2023
@CanRau
Copy link
Author

CanRau commented Oct 30, 2023

So I have multiple polymorphic tables, like commenting which has commentable_type & commentable_id to reference basically any row in any other table because we have a couple of tables which are "commentable"

Then we have likes, tagging tables which work the same and an assetable which basically works the same though joins an asset with whatever other table while also adding more meta data

If hooks can solve this that would be great, otherwise I'd have to pass. Which would also be totally fine, I'm not yet even sure what to use and it's also your project ❤️

With Drizzle it's already possible though they seem to be adding the option to specify a where clause on relation creation like so

export const usersRelations = relations(users, ({ many }) => ({
  comments: many(comments, {
    where: eq(comments.commentType, 'user')
  }),
}));

which from an api perspective might look like this in rdb

order: x.user.map(({ hasOne, hasMany, references }) => ({
    likes: hasMany(x.likes).by('likeableId').where(x.likes.likeableType.eg(x.user.id))
  }))

or something like that 🤔

@lroal
Copy link
Member

lroal commented Oct 31, 2023

This can be solved by using the columns disciminators. It hasn't been documented yet, but your case would make an excellent example. When setting the column discriminator to commentable_type='user', any comment that is inserted will set the column commentable_type to value 'user' automatically. And vice versa for selecting.

One thing I notice, is that you would like to use the column, commentable_id, for both referencing city and user. This will work, but you will not be able to have a physical FK constraint in the database. A better solution would be to have explicit fields for userId and cityId in the comments table. This will also make it easy to fetch directly in comments table and traverse to correct parent. I propose both solutions below.

With same column for parent table:

const map = rdb.map((x) => ({
    user: x.table('user').map(({ column }) => ({
      id: column('id').numeric().primary(),
      name: column('name').string(),
    })),

    city: x.table('city').map(({ column }) => ({
      id: column('id').numeric().primary(),
      name: column('name').string(),
    })),

    userLikes: x.table('comment').map(({ column }) => ({
        id: column('id').numeric().primary(),
        userId: column('commentable_id').numeric().notNull(),
      }))
      .columnDiscriminators(`commentable_type='user'`),

    cityTags: x.table('comment').map(({ column }) => ({
        id: column('id').numeric().primary(),
        cityId: column('commentable_id').numeric().notNull(),
      }))
      .columnDiscriminators(`commentable_type='city'`),
  }))
  .map((x) => ({
    user: x.user.map(({ hasMany }) => ({
      likes: hasMany(x.userLikes).by('userId'),
    })),
    city: x.city.map(({ hasMany }) => ({
      tags: hasMany(x.cityTags).by('cityId'),
    })),
  }));

Optimal solution with explicit columns for city and user:

const map = rdb
  .map((x) => ({
    user: x.table('user').map(({ column }) => ({
      id: column('id').numeric().primary(),
      name: column('name').string(),
    })),

    city: x.table('city').map(({ column }) => ({
      id: column('id').numeric().primary(),
      name: column('name').string(),
    })),

    userLikes: x.table('comment').map(({ column }) => ({
        id: column('id').numeric().primary(),
        userId: column('user_id').numeric().notNull(),
      }))
      .columnDiscriminators(`commentable_type='user'`),

    cityTags: x.table('comment').map(({ column }) => ({
        id: column('id').numeric().primary(),
        cityId: column('city_id').numeric().notNull(),
      }))
      .columnDiscriminators(`commentable_type='city'`),

    comments: x.table('comment').map(({ column }) => ({
      id: column('id').numeric().primary(),
      cityId: column('city_id').numeric(),
      userId: column('user_id').numeric(),
      type: column('commentable_type').string(),
    }))    
  }))
  .map((x) => ({
    user: x.user.map(({ hasMany }) => ({
      likes: hasMany(x.userLikes).by('userId'),
    })),

    city: x.city.map(({ hasMany }) => ({
      tags: hasMany(x.cityTags).by('cityId'),
    })),

    comments: x.comments.map(({ references }) => ({
      user: references(x.user).by('userId'),
      cityId: references(x.user).by('cityId'),
    }))
  }));

@lroal lroal added question and removed duplicate labels Oct 31, 2023
@CanRau
Copy link
Author

CanRau commented Oct 31, 2023

Interesting I'll give that a try when I get the time 😃

Foreign key constraints aren't supported by Vitess and therefore Planetscale so not an issue 😄

Edit: now I'm curious what you were thinking initially with custom row logic etc?

@lroal
Copy link
Member

lroal commented Nov 1, 2023

With custom row logic I was thinking that you could just wrap a Class around the row - like a proxy. In that way, the row would have methods as well. Assume customer row has email. Then add a method notifyUnpaidInvoices() the Customer class that sends email about unpaid Invoices.
The Class that is returned must play together with intellisense so that getOne wil return that Class unioned with the Row type. I need to do some research how this would look like.

@CanRau
Copy link
Author

CanRau commented Nov 2, 2023

Ah that sounds very interesting 😃
Still haven't really had the time to play with rdb 🥲

@lroal
Copy link
Member

lroal commented Nov 2, 2023

Enjoy 🙂
I am closing this issue for now

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

No branches or pull requests

2 participants