How to solve combined one-to-one and one-to-many r

2019-04-01 18:04发布

问题:

I´m using Entity Framework 5 and Code First.

I have two domain entities Question and Answer for a quiz application. One question has several possible answers. A question also has one correct answer which should reference one of the possible answers. I am experiencing some issues with the combination of a one-to-many and one-to-one relationship between the to entities. See Q1 and Q2.

This is the code for the entities:

public class Question
{
    public virtual int Id { get; set; }
    [Required]
    public virtual string Text { get; set; }

    [InverseProperty("Question")] 
    public virtual ICollection<Answer> PossibleAnswers { get; set; }
    public virtual Answer CorrectAnswer { get; set; }        

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public virtual DateTime? UpdateStamp { get; set; }
}

public class Answer
{
    public virtual int Id { get; set; }
    [Required]
    public virtual string Text { get; set; }

    [ForeignKey("QuestionId")]
    public virtual Question Question { get; set; }
    public virtual int QuestionId { get; set; }
}

Q1: What should I do to be able to insert the Question object and referenced Answers (thru property PossibleAnswers) in just one roundtrip to the db (eg one call to the contexts SaveChanges)? The error I get when I save the Questions and Answers without adding the Answers first is:

Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.

To solve that problem I tried the following using fluent API to get the Answers to be added prior to Questions when doing it all with just one call the objectcontexts SaveChanges:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Question>()
            .HasOptional(q => q.CorrectAnswer)
            .WithRequired();

        base.OnModelCreating(modelBuilder);
    }

However, that lead me to another error:

Conflicting changes detected. This may happen when trying to insert multiple entities with the same key.

Am I on the right path with the fluent API approach for Q1? Why the error message?

Q2: When deleting a question I realize that there will be an error since the question cannot be deleted before the answers and vice versa. How do I solve this? For instance, is WillCascadeOnDelete supposed to be specified on both Question.CorrectAnswer and Question.PossibleAnswers?

回答1:

For both your questions Q1 and Q2 you will need two roundtrips/two calls to SaveChanges (aside from solving the problem with a Stored Procedure maybe).

Q1: The first call having Question.CorrectAnswer set to null and a second call that sets the CorrectAnswer to one of the stored answers.

Q2: The first call setting Question.CorrectAnswer to null and the second call deletes the Question and the related answers with enabled cascading delete.

If you are not so worried about the two roundtrips but more about the two transactions corresponding to the two SaveChanges calls you can wrap the whole operation including the two SaveChanges calls into a single manual transaction. (Example: EF: How do I call SaveChanges twice inside a transaction?)

About the one-to-one relationship: Although from business perspective the relationship for the CorrectAnswer is one-to-one it is hard or even impossible to model it as a one-to-one relationship with EF.

The problem is that EF does not support foreign key one-to-one associations, i.e. relationships where the foreign key (CorrectAnswerId or so) has a unique constraint. It only supports shared primary key one-to-one associations where the primary key of the dependent (Question) is the foreign key (for Question.CorrectAnswer) to the principal (Answer) at the same time. Your Fluent code is the configuration for such a shared primary key association. But it would mean that the only valid CorrectAnswer is the Answer that has the same primary key value as the Question. While this is theoretically possible to achieve (your Answer table has more records than the Question table) it would most likely require to not use autogenerated keys but supply the keys manually. Changing the CorrectAnswers from one Answer to the other would be impossible. So, shared primary keys are not suited for your model in my opinion.

The better solution would be to remove your Fluent mapping. The result would be a one-to-many relationship with a nullable foreign key for CorrectAnswer in the Question table. From database perspective it means that the same Answer can be the CorrectAnswer for many Questions which is probably nonsense in your business logic because every question has its own unique set of answers and two questions never share the same answer. But you can "hide" this one-to-many relationship from your business logic by just not adding an inverse collection property (like QuestionsThisIsTheCorrectAnswerFor) to Answer. Although it does not model the business constraint perfectly it works technically without problems.

More about the difficulties of one-to-one relationships with EF can be found in these blog posts:

  • Shared Primary Key One-to-One Associations
  • One-to-One Foreign Key Associations