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

MongoDB Optional One-to-One #484

Closed
iamomiid opened this issue Apr 16, 2020 · 12 comments · Fixed by #485
Closed

MongoDB Optional One-to-One #484

iamomiid opened this issue Apr 16, 2020 · 12 comments · Fixed by #485
Assignees
Labels
bug Something isn't working

Comments

@iamomiid
Copy link
Contributor

Describe the bug
One-to-one relations add unique indexes in MongoDB, but sparse option should be also added for optional attributes.

To Reproduce
Steps to reproduce the behavior:

  1. Create a one-to-one relation.
  2. Add multiple documents keeping optional attribute undefined.
  3. Unique index will raise exception.

Expected behavior
Multiple documents with undefined optional attribute should be ok.

@iamomiid iamomiid added the bug Something isn't working label Apr 16, 2020
@B4nan
Copy link
Member

B4nan commented Apr 16, 2020

Could you elaborate a bit on "sparse option should be also added for optional attributes"?

It's been a while since I used mongo in real world (and I am no expert when it comes to mongo indexes), so it would help me understand this faster.

@iamomiid
Copy link
Contributor Author

iamomiid commented Apr 16, 2020

If there is a unique index on collection, there can't be two documents having null attributes. Sparse option basically skips any documents with the missing field. More information can be found on MongoDB Manual: Sparse indexes.
I achieve this behavior on mikro-orm by using

@Unique({options: { sparse: true }})

@iamomiid
Copy link
Contributor Author

This behavior should be added to optional properties with one-to-one decorator

@iamomiid
Copy link
Contributor Author

I'm a little bit lost in source code. Should createPropertyIndexes method be altered to achieve this behavior?

@B4nan
Copy link
Member

B4nan commented Apr 16, 2020

Yes, we need to handle this exactly there, as there are more ways to define entities than just via decorators. Should be just about passing the sparse option when the prop is 1:1 and nullable, right?

Indexes on the entity level are stored in meta.indexes, but they will be inlined to the properties when possible (basically the first index for given property with just a name, no additional options), that happens here:

https://github.com/mikro-orm/mikro-orm/blob/master/lib/metadata/MetadataDiscovery.ts#L566

@iamomiid
Copy link
Contributor Author

Yes, that's right.
I'm wandering through source code but still can't figure out where should be edited except the createPropertyIndexes method.

@B4nan
Copy link
Member

B4nan commented Apr 16, 2020

I think that is the only place.

@eoanodea
Copy link

Could you possibly add something to the docs about this? I'm running into a similar issue and not sure how to approach resolving it. Thanks

@B4nan
Copy link
Member

B4nan commented Feb 25, 2021

Looking at the PR that closed this, things should work automatically as long as you have nullable: true in the property options.

Maybe you will need to remove existing index first, not sure. Can you share more details on what you are facing?

@eoanodea
Copy link

I have an OneToOne relationship between two entities - QuestionText & Note. I want Note to be optional, but every time I attempt to create one I'll get a Mongo E11000 duplicate key error saying note: null already exists.

// QuestionText Entity

@ObjectType()
@Entity()
export class QuestionText extends Base<QuestionText> {
  @Field()
  @Property()
  public text: string;

  @Field(() => Note, { nullable: true })
  @OneToOne(() => Note)
  public note?: Note;

  @Field(() => Question)
  @ManyToOne(() => Question, { onDelete: "cascade" })
  public question: Question;

  constructor(body: QuestionTextValidator) {
    super(body);
  }
}

// Note Entity
@ObjectType({ description: "Represents a note within the database" })
@Entity()
export class Note extends Base<Note> {
  @Field()
  @Property()
  public text: string;

  @Field(() => QuestionText)
  @OneToOne(() => QuestionText, (b: QuestionText) => b.note)
  public questionText: QuestionText;

  constructor(body: NoteValidator) {
    super(body);
  }
}

If I attempt to run a mutation with the following input:

[
        {text: "question text 1" },
        {text: "question text 2"}
]

The first item is created within my DB, but the second is not and the duplicate key error appears

// resolver logic
      if (input.text.length > 0) {
        for (let text of input.text) {
          const newQuestionText = new QuestionText(text);
          if (text.note) {
            const note = await ctx.em
              .getRepository(Note)
              .findOneOrFail(text.note);
            newQuestionText.note = note;
          }

          await ctx.em.persist(newQuestionText);
        }
      }

Any suggestions would be greatly appreciated

@B4nan
Copy link
Member

B4nan commented Feb 25, 2021

As said, you need to mark is as nullable:

  @OneToOne({ entity: () => Note, nullable: true })
  public note?: Note;

Then you might need to first remove the existing index manually.

@eoanodea
Copy link

You're a lifesaver! I've been looking at this for far too long. Thanks so much 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants