简体   繁体   中英

EF 6: inserting multiple - The relationship could not be changed because one or more of the foreign-key properties is non-nullable

I've run across this a few times in our system in the past, but haven't found the reasoning...

In essence I have a Contact, and the contact has a nullable PhoneNumber.

The code Loops through a list of View Models, maps them to the Data Models, then (after all models are mapped) calls SaveChanges().

The test data has the data for the phone number duplicated. When I alter the test data to have different phone numbers for each contact, everything saves fine.

Here's a stripped down simple sample of what I'm doing that exhibits the behavior.

class Program
{
    static void Main(string[] args)
    {
        TestWeirdEDMXIssue();
        Console.ReadLine();
    }

    static void TestWeirdEDMXIssue()
    {
        List<dynamic> works = new List<dynamic>
            {
                new { FirstName = "Fred", LastName = "Snider", PhoneNumber = "888-888-8881", CountryID = 1 },
                new { FirstName = "Ted", LastName = "Snider", PhoneNumber = "888-888-8882", CountryID = 1 },
                new { FirstName = "Mike", LastName = "Snider", PhoneNumber = "888-888-8883", CountryID = 1 },
                new { FirstName = "Tim", LastName = "Snider", PhoneNumber = "888-888-8884", CountryID = 1 },
                new { FirstName = "Todd", LastName = "Snider", PhoneNumber = "888-888-8885", CountryID = 1 },
                new { FirstName = "Terry", LastName = "Snider", PhoneNumber = "888-888-8886", CountryID = 1 }
            };


        List<dynamic> broken = new List<dynamic>
            {
                new { FirstName = "Fred", LastName = "Snider", PhoneNumber = "888-888-8888", CountryID = 1 },
                new { FirstName = "Ted", LastName = "Snider", PhoneNumber = "888-888-8888", CountryID = 1 },
                new { FirstName = "Mike", LastName = "Snider", PhoneNumber = "888-888-8888", CountryID = 1 },
                new { FirstName = "Tim", LastName = "Snider",  PhoneNumber = "888-888-8888", CountryID = 1 },
                new { FirstName = "Todd", LastName = "Snider", PhoneNumber = "888-888-8888", CountryID = 1 },
                new { FirstName = "Terry", LastName = "Snider", PhoneNumber = "888-888-8888", CountryID = 1 }
            };

        TestWeirdEDMXIssueSave(works); //Completes with "Success!"
        TestWeirdEDMXIssueSave(broken); //Throws Exception

    }

    static void TestWeirdEDMXIssueSave(List<dynamic> data)
    {
        try
        {

            using (var context = new DBEntities())
            {
                var creatorID = context.UserProfiles.FirstOrDefault(up =>  up.Contact.FirstName == "automationtestuser")?.ID  ?? Guid.Empty;
                foreach (var item in data)
                {
                    var contact = context.Contacts.Create();
                    context.Contacts.Add(contact);

                    contact.ID = Guid.NewGuid();
                    contact.FirstName = item.FirstName;
                    contact.LastName = item.LastName;
                    contact.CreationDate = DateTime.Now;
                    contact.CreatedBy = creatorID;


                    var phoneNumber = context.PhoneNumbers.Create();
                    context.PhoneNumbers.Add(phoneNumber);

                    //PhoneNumber ID is Identity
                    phoneNumber.CreatedBy = creatorID;
                    phoneNumber.CreationDate = DateTime.Now;
                    phoneNumber.TypeID = (int)PhoneNumberTypes.Office;
                    phoneNumber.Number = item.PhoneNumber;
                    phoneNumber.CountryID = item.CountryID;

                    contact.PhoneNumber = phoneNumber;
                }
                context.SaveChanges();
            }
            Console.WriteLine("Success!");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
}

OUTPUT

Success!

System.InvalidOperationException: The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
    at System.Data.Entity.Core.Objects.ObjectContext.PrepareToSaveChanges(SaveOptions options)
    at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesInternal(SaveOptions options, Boolean executeInExistingTransaction)
    at System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(SaveOptions options)
    at System.Data.Entity.Internal.InternalContext.SaveChanges()
    at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
    at System.Data.Entity.DbContext.SaveChanges()
...(My Code)...

EDIT Here are the create scripts for the Tables (sql generated, so its a bit ugly):

Contact

CREATE TABLE [Common].[Contacts](
    [ID] [uniqueidentifier] NOT NULL,
    [FirstName] [varchar](25) NOT NULL,
    [MiddleInitial] [char](1) NULL,
    [LastName] [varchar](50) NOT NULL,
    [Title] [varchar](50) NULL,
    [Description] [varchar](255) NULL,
    [ContactEmailAddress] [varchar](100) NULL,
    [ContactPhoneNumberID] [int] NULL,
    [ContactFaxNumberID] [int] NULL,
    [AddressID] [int] NULL,
    [ACHDate] [datetime] NULL,
    [CreatedBy] [uniqueidentifier] NOT NULL,
    [CreationDate] [datetime] NOT NULL,
    [IsMarkedAsDeleted] [bit] NOT NULL,
    [Position] [varchar](50) NULL,
    [MiddleName] [varchar](50) NULL,
    [LastThenFirstName]  AS (case when [LastName] IS NULL OR len(rtrim([LastName]))=(0) then [FirstName] when [FirstName] IS NULL OR len(rtrim([FirstName]))=(0) then [LastName] else (rtrim(ltrim([LastName]))+', ')+rtrim(ltrim([FirstName])) end) PERSISTED,
    [FirstThenLastName]  AS (rtrim(ltrim((isnull([FirstName],'')+' ')+isnull([LastName],'')))) PERSISTED,
    [AuthenticatorPin] [int] NULL,
    [AuthenticatorCode] [int] NULL,
 CONSTRAINT [PK_Contacts] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [Common].[Contacts] ADD  CONSTRAINT [DF_Contacts_ID]  DEFAULT (newid()) FOR [ID]
GO

ALTER TABLE [Common].[Contacts] ADD  CONSTRAINT [DF_Contacts_CreationDate]  DEFAULT (getdate()) FOR [CreationDate]
GO

ALTER TABLE [Common].[Contacts] ADD  CONSTRAINT [DF_Contacts_IsMarkedAsDeleted]  DEFAULT ((0)) FOR [IsMarkedAsDeleted]
GO

ALTER TABLE [Common].[Contacts]  WITH CHECK ADD  CONSTRAINT [FK_Contacts_AddressID_Addresses_ID] FOREIGN KEY([AddressID])
REFERENCES [Common].[Addresses] ([ID])
GO

ALTER TABLE [Common].[Contacts] CHECK CONSTRAINT [FK_Contacts_AddressID_Addresses_ID]
GO

ALTER TABLE [Common].[Contacts]  WITH CHECK ADD  CONSTRAINT [FK_Contacts_ContactFaxNumberID_PhoneNumbers_ID] FOREIGN KEY([ContactFaxNumberID])
REFERENCES [Common].[PhoneNumbers] ([ID])
GO

ALTER TABLE [Common].[Contacts] CHECK CONSTRAINT [FK_Contacts_ContactFaxNumberID_PhoneNumbers_ID]
GO

ALTER TABLE [Common].[Contacts]  WITH CHECK ADD  CONSTRAINT [FK_Contacts_ContactPhoneNumberID_PhoneNumbers_ID] FOREIGN KEY([ContactPhoneNumberID])
REFERENCES [Common].[PhoneNumbers] ([ID])
GO

ALTER TABLE [Common].[Contacts] CHECK CONSTRAINT [FK_Contacts_ContactPhoneNumberID_PhoneNumbers_ID]
GO

ALTER TABLE [Common].[Contacts]  WITH CHECK ADD  CONSTRAINT [FK_Contacts_CreatedBy_UserProfiles_ID] FOREIGN KEY([CreatedBy])
REFERENCES [Common].[UserProfiles] ([ID])
GO

ALTER TABLE [Common].[Contacts] CHECK CONSTRAINT [FK_Contacts_CreatedBy_UserProfiles_ID]
GO

Phone Numbers

CREATE TABLE [Common].[PhoneNumbers](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [Number] [varchar](20) NOT NULL,
    [Extension] [varchar](10) NULL,
    [TypeID] [int] NOT NULL,
    [CountryID] [int] NOT NULL,
    [CreatedBy] [uniqueidentifier] NOT NULL,
    [CreationDate] [datetime] NOT NULL,
    [IsMarkedAsDeleted] [bit] NOT NULL,
 CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [Common].[PhoneNumbers] ADD  CONSTRAINT [DF_PhoneNumbers_CreationDate]  DEFAULT (getdate()) FOR [CreationDate]
GO

ALTER TABLE [Common].[PhoneNumbers] ADD  CONSTRAINT [DF_PhoneNumbers_IsMarkedAsDeleted]  DEFAULT ((0)) FOR [IsMarkedAsDeleted]
GO

ALTER TABLE [Common].[PhoneNumbers]  WITH CHECK ADD  CONSTRAINT [FK_PhoneNumbers_CountryID_Countries_ID] FOREIGN KEY([CountryID])
REFERENCES [Common].[Countries] ([ID])
GO

ALTER TABLE [Common].[PhoneNumbers] CHECK CONSTRAINT [FK_PhoneNumbers_CountryID_Countries_ID]
GO

ALTER TABLE [Common].[PhoneNumbers]  WITH CHECK ADD  CONSTRAINT [FK_PhoneNumbers_CreatedBy_UserProfiles_ID] FOREIGN KEY([CreatedBy])
REFERENCES [Common].[UserProfiles] ([ID])
GO

ALTER TABLE [Common].[PhoneNumbers] CHECK CONSTRAINT [FK_PhoneNumbers_CreatedBy_UserProfiles_ID]
GO

ALTER TABLE [Common].[PhoneNumbers]  WITH CHECK ADD  CONSTRAINT [FK_PhoneNumbers_TypeID_PhoneNumberTypes_ID] FOREIGN KEY([TypeID])
REFERENCES [Common].[PhoneNumberTypes] ([ID])
GO

ALTER TABLE [Common].[PhoneNumbers] CHECK CONSTRAINT [FK_PhoneNumbers_TypeID_PhoneNumberTypes_ID]
GO

Depending on how your entities are mapped and the relationship between PhoneNumber and Contact, the data may not be persisting as you expect. Ideally you should use database generated IDs for all entities and use navigation properties to manage the related entities.

Based on the examples, check that the schema has a PhoneNumberId column on Contact, and that it is a FK to the PhoneNumber table's PK. Also check that the relationship between contact and phone number is set up correctly. For instance either the entity type configuration or DbContext OnModelCreating you should have something like:

If your Contact entity declares a PhoneNumberId FK field:

HasRequired(x => x.PhoneNumber)
  .WithMany()
  .HasForeignKey(x => x.PhoneNumberId); 

or if the entity does not declare the FK field, but the table has the FK:

HasRequired(x => x.PhoneNumber)
  .WithMany()
  .Map(x => x.MapKey("PhoneNumberId")); 

This forms a many-to-one relationship between contact and phone # which may not be intended since it is possible to use the same phone # record against multiple contacts. (2 contacts may share the same phone #, but changing the phone # on one should not necessarily update the # of the other.) For a 1-to-1 relationship the PhoneNumber table would best be served by using a ContactId as its PK.

IF this relationship is not set up appropriately one way or the other, EF can end up inserting records in the wrong order, not recognizing the required FKs.

In terms of populating your entities, you can be less deliberate with the entity creation and let EF manage the related data:

using (var context = new DBEntities())
{
  // A better way to select the ID rather than returning the entire entity:
  var creatorID = context.UserProfiles
    .Where(up =>  up.Contact.FirstName == "automationtestuser")
    .Select(up => up.ID)
    .SingleOrDefault();

  foreach (var item in data)
  {
    var contact = new Contact
    {
      //contact.ID = Guid.NewGuid(), Definitely recommend letting DB generate ID using NewSequentialId.
      FirstName = item.FirstName,
      LastName = item.LastName,
      CreationDate = DateTime.Now,
      CreatedBy = creatorID,

      PhoneNumber = new PhoneNumber 
      {
        CreatedBy = creatorID;
        CreationDate = DateTime.Now;
        TypeID = (int)PhoneNumberTypes.Office;
        Number = item.PhoneNumber;
        CountryID = item.CountryID;
      }
    }
    context.Contacts.Add(contact);
    context.SaveChanges(); 
  }
}

Note that we don't need to create or add phone #'s directly to the DbContext. EF will take care of that using the appropriate relationships and set the FKs for us.

This ensures that the related entities are inserted together and you can avoid adding a DbSet for every entity into your DbContext. (Do you need to search for PhoneNumbers as a top level entity?)

However, to fully determine what is going wrong you should also post any mapping declarations you have, whether code-first annotations, or schema first with entity type configurations or OnModelCreating overrides.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM