简体   繁体   中英

Entity Framework 5: Code-First Cyclical Relationship Issues

I understand why EF does not allow "cyclical references" in the PK/FK relationships. I am looking for advice on how to alter my model to make the following scenario work.

Scenario

Three entities: Employee , Agency , WorkRecord . Their purpose is to log Employee time spent doing work. Employee then contains reference to the Agency he/she is employed by, and his/her WorkRecord contain reference to the Agency the work was done for.

public class Employee
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public int AgencyId { get; set; }
    public virtual Agency Agency { get; set; }

    public virtual IEnumerable<WorkRecord> WorkRecords { get; set; }
}

public class Agency
{
    [Key]
    public int Id { get; set; } 
}

public class WorkRecord
{
    [Key]
    public int Id { get; set; } 

    public int Hours { get; set; } 

    public int AgencyId { get; set; } 
    public virtual Agency Agency { get; set; } 

    public int EmployeeId { get; set; }
    public virtual Employee { get; set; }
}

Like this, it bitches: FK_dbo.WorkRecords_dbo.Employees_EmployeeId causes a cyclical reference.

Experiments

My first thought was because of the bi-directional virtual properties, so I decided to designate one of the two a top-level entity with a 1-way relationship:

First, I designated WorkRecord as a top-level entity and remove the virtual WorkRecords reference reference from the Employee entity... the same message is produced.

Second, I made Employee the top-level entity, leaving its virtual WorkRecords collection, and removing the virtual Employee reference property from the WorkRecord entity... works fine but does not achieve my goal.

After more investigation, I find it is the Agency virtual reference property on both entities that causes the circular reference. If one entity removes this, the Employee / WorkRecord entity relationships work in all directions.

Question:

So, clear as i can ask - how can I express this business model, using WorkRecord as my top-level entity, without making EF5 upset?

It sounds like you just want to get EF off your back, but I think it's actually expressing a valid problem in the coupling of your data. If you bind AgencyId to both WorkRecord and Employee then updating the AgencyId on WorkRecord, for example, will cascade to Employee. Which will then cascade to WorkRecord etc. Hence "circular reference". You really should designate which of those data objects will "own" the relationship to Agency.

Personally, I suspect that the most natural binding is to reference the Agency from the WorkRecord. I can see a scenario where an Employee might move from one agency to another but it'd be much harder for a WorkRecord to move from one Agency to another. It's also the case that an Employee without a WorkRecord can't really be termed much of an Employee, really. If you determine this to be the case, then I'd remove the Agency reference from Employee. If you need to get to the Agency from the Employee then you probably should go through a WorkRecord anyway.

All of that is merely conceptual, however. I suspect that if you make it possible for AgencyId to be null on the Employee that EF won't complain any longer (and you might want it optional on both). That should make it valid for an Employee to be updated without requiring a circular update with WorkRecord. I'd have to test that to verify, but I suspect it'd hold true.

public class Employee
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public int? AgencyId { get; set; }
    public virtual Agency Agency { get; set; }

    public virtual IEnumerable<WorkRecord> WorkRecords { get; set; }
}

You probably get an exception from SQL Server, not Entity Framework, like:

Introducing FOREIGN KEY constraint 'ABC' on table 'XYZ' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

This exception basically says what you need to do to fix the problem: "Specifying ON DELETE NO ACTION" means disabling cascading delete for at least one of the relationships. The problem is that all three relationships are required because your foreign key properties AgencyId and EmployeeId are non-nullable. In this case EF will create the relationships in the database with enabled delete. The result is a multiple delete path when you would delete an Agency : It would delete the WorkRecords and the Employees, but the Employees will delete the Workrecords as well, so you have two multiple delete paths on WorkRecords.

You can disable cascading delete only with Fluent API:

modelBuilder.Entity<Employee>()
    .HasRequired(e => e.Agency)
    .WithMany()
    .HasForeignKey(e => e.AgencyId);

modelBuilder.Entity<WorkRecord>()
    .HasRequired(w => w.Agency)
    .WithMany()
    .HasForeignKey(w => w.AgencyId)
    .WillCascadeOnDelete(false); // or for one or more of the other relationships

modelBuilder.Entity<WorkRecord>()
    .HasRequired(w => w.Employee)
    .WithMany(e => e.WorkRecords)
    .HasForeignKey(w => w.EmployeeId);

Deleting an Agency now causes the related employees to be deleted and the deleted employees will cause the related workrecords to be deleted. But the agency won't directly delete the workrecords anymore, thus removing the second delete path.

You can alternatively make one of relationships optional which disables cascading delete automatically by convention (see Jacob Proffitt's answer).

BTW: You can't use an IEnumerable<T> for a navigation property, you must use ICollection<T> or a derived interface or implementation.

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