prisma / prisma

Next-generation ORM for Node.js & TypeScript | PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, MongoDB and CockroachDB
https://www.prisma.io
Apache License 2.0
39.71k stars 1.55k forks source link

MongoDB one-way embedding (one-way, one-sided) relations #12725

Open paulrostorp opened 2 years ago

paulrostorp commented 2 years ago

Problem

When using the mongoDB I find it problematic that many-to-many relations have to be stored on "both sides".

An example use case:

model Post {
  id      String   @id
  users User[]   @relation(fields: [userIds], references: [id])
  userIds String[]
}

model User {
  id           String   @id
  likes        Post[]   @relation(references: [id], fields: [likedPostIds])
  likedPostIds String[]
}

This simple data model would allow me to use prisma's nice Fluent API, and do things like User.likes(). However, this is not scalable at all, if a Post has a million likes, the userIds array is going to be huge.

Suggested solution

Ideally, I would like to have the flexibility to decide for myself how the data is going to be structured. For my use case, I will never need to follow the relation from Post=>User, only User=>Post.

I wish the formatter would allow one-way relations, that way it could be as simple as:

model Post {
  id String @id
}

model User {
  id           String   @id
  likes        Post[]   @relation(references: [id], fields: [likedPostIds])
  likedPostIds String[]
}

or (with the back relation):

model Post {
  id String @id
  users User[]
}

model User {
  id           String   @id
  likes        Post[]   @relation(references: [id], fields: [likedPostIds])
  likedPostIds String[]
}

Alternatives

The only alternative is to have a very granular "join table", such as this:

model Post {
  id String @id @map("_id")

  LikesByUser LikesByUser[]
}

model LikesByUser {
  userId String
  post   Post   @relation(fields: [postId], references: [id])
  postId String
  User   User   @relation(fields: [userId], references: [id])

  @@id([userId, postId])
}

model User {
  id          String        @id @map("_id")
  LikesByUser LikesByUser[]
}

Other than inconvenience, the downside of doing this is that it isn't very storage efficient.

dimaip commented 2 years ago

Here's a good explanation why two-way embeds won't work well for large sets of items: https://learnmongodbthehardway.com/schema/schemabasics/#one-way-embedding

So yes, it's a rather big gap in terms of data modelling for Mongo, so would be awesome if this issue could be prioritised 🙏

For my use case, I will never need to follow the relation from Post=>User, only User=>Post.

Actually the relation can be followed both ways just fine in a performant way thanks to Mongo's query engine (given you have an index on that field).

janpio commented 2 years ago

Note: I added a second one-way-embed PSL example to the initial issue description that includes the backrelation, as that is something we would most probably add for this type of relation. Currently you can only have relations that are present in both directions (in Prisma Client, the underlying data is independent from that).

MinSomai commented 2 years ago

any update on this?

dimaip commented 2 years ago

Sorry, missed your reply @janpio!

in Prisma Client, the underlying data is independent from that

Could you please clarify what this means? Does it mean that the data only has to be present on one side, and it'd be traversable both ways?

janpio commented 2 years ago

Currently in MongoDB m:n relations, the data has to be present on both sides. But in relational databases, that is not the case - the foreign key(s) can be on one side and the relation can be navigated in both directions anyway.

dimaip commented 2 years ago

Oh, I see. That gave me a false glimpse of hope lol.

Currently, it's very hard to adopt Prisma to work with existing Mongo projects, without having to adjust the whole data structure (which is not easily possible for large production apps).

This issue and lack of union types support are the two biggest pain points for us which make us struggle immensely and regret that we have invested so much time into adopting Prisma, perhaps a bit too prematurely.

janpio commented 2 years ago

Do issues for these two problems exist with appropriate explanations why you need these use cases?

dimaip commented 2 years ago

Do issues for these two problems exist with appropriate explanations why you need these use cases?

https://github.com/prisma/prisma/issues/2505 for union types. For new functionality we could use a dedicated collection per type and join them into a union type on GraphQL level, but we can't do that for legacy code, would be too much risk re-organising legacy code...

Regarding this issue, the article I linked here explains the pros and cons pretty in-depth https://github.com/prisma/prisma/issues/12725#issuecomment-1175264009 In addition to that, it's also hard to adjust our legacy code to write both sides of the relations.

So yeah, fitting Prisma on top of an existing Mongo/Mongoose application is not an easy fit...

MinSomai commented 2 years ago
 const fooSchema = new mongoose.Schema({
  bar: { type: mongoose.Schema.ObjectId, ref: 'Bar required: true },
  bazz: [{ type: mongoose.Schema.ObjectId, ref: 'Bazz }],
});

Having difficulty using prisma on top of existing project. In the example above. bar is easy to implement, but there is no way to populate bazz because Bazz collection has no idea about fooSchema.

technotip commented 1 year ago

After trying to achieve this(one-way / one-sided / many-to-one) for more than 10 days, I finally found this issue. I had also asked similar questions here: https://github.com/prisma/prisma/discussions/16609

Please implement this feature.

okanji commented 1 year ago

This is a very important issue. One would assume that this would be possible only to end up getting stuck. Thanks for working on this and looking forward to this being solved

okanji commented 1 year ago

You could try something like this, it worked for me:

schema.prisma

model User {
  id                                 String                @id @default(auto()) @map("_id") @db.ObjectId
  email                              String                @unique
  privateCloudProjectOwner           PrivateCloudProject[] @relation("projectOwner")
  privateCloudProjectOwnerIds        String[]              @map("privateCloudProjectOwner") @db.ObjectId
 }

model PrivateCloudRequest {
  id             String    @id @default(auto()) @map("_id") @db.ObjectId
  projectOwnerId String    @db.ObjectId
  projectOwner   User      @relation("requestProjectOwner", fields: [projectOwnerId], references: [id])
}

createPrivateCloudRequest.js

const createRequest = await prisma.privateCloudRequest.create({
    data: {
      projectOwner: {
        connectOrCreate: {
          where: {
            email: "existingemail@test.com"
          },
          create: {
            email: "newemail@test.com",
          }
        }
      }

    }
  });

... I removed some fields above as they are not relevant. But you will see them in my monodb atlas

image

image

@technotip maybe give this a try..

technotip commented 1 year ago

@okanji for my use case, I actually solved it by having another collection.

The use case: There are User, Post collections. Each post should have an array of all the user objects who liked the post. That way whenever someone visits a post, I can show the users who liked the post. So I need a way to store all the object ids in an array and I should be able to retrieve the user data based on their object id in the array.

I solved it by having a 3rd collection just to store the postId from Post collection and the userId of user who liked particular post.

okanji commented 1 year ago

@technotip makes sense! Glad you found a solution!

rusprice commented 1 year ago

Hello. I and my team write in support of this. We were going to switch from Mongoose to Prisma, only to find out that one way relationships aren't supported, which we'd prefer to have, and so that means having to choose another ORM/keep using Mongoose.