alfateam/orange-orm

Polymorphic Associations

CanRau opened this issue ยท 9 comments

CanRau commented

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 commented

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 commented

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 commented

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

CanRau commented

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 commented

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'),
    }))
  }));
CanRau commented

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 commented

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 commented

Ah that sounds very interesting ๐Ÿ˜ƒ
Still haven't really had the time to play with rdb ๐Ÿฅฒ

lroal commented

Enjoy ๐Ÿ™‚
I am closing this issue for now