Relazioni, proprietà di navigazione e chiavi esterne

Questo articolo offre una panoramica del modo in cui Entity Framework gestisce le relazioni tra entità. Fornisce anche alcune indicazioni su come eseguire il mapping e la modifica delle relazioni.

Relazioni in Entity Framework

Nei database relazionali, le relazioni (dette anche associazioni) tra le tabelle vengono definite tramite chiavi esterne. Una chiave esterna (FK) è una colonna o una combinazione di colonne usate per stabilire e applicare un collegamento tra i dati in due tabelle. Esistono in genere tre tipi di relazioni: uno-a-uno, uno-a-molti e molti-a-molti. In una relazione uno-a-molti, la chiave esterna viene definita nella tabella che rappresenta la maggior parte della relazione. La relazione molti-a-molti implica la definizione di una terza tabella (denominata tabella di giunzione o join), la cui chiave primaria è costituita dalle chiavi esterne di entrambe le tabelle correlate. In una relazione uno-a-uno, la chiave primaria funge anche da chiave esterna e non esiste una colonna chiave esterna separata per entrambe le tabelle.

L'immagine seguente mostra due tabelle che partecipano alla relazione uno-a-molti. La tabella Course è la tabella dipendente perché contiene la colonna DepartmentID che lo collega alla tabella Department .

Department and Course tables

In Entity Framework un'entità può essere correlata ad altre entità tramite un'associazione o una relazione. Ogni relazione contiene due estremità che descrivono il tipo di entità e la molteplicità del tipo (uno, zero-o-uno o molti) per le due entità in tale relazione. La relazione può essere governata da un vincolo referenziale, che descrive quale fine della relazione è un ruolo principale e che è un ruolo dipendente.

Le proprietà di navigazione consentono di esplorare un'associazione tra due tipi di entità. Ogni oggetto può disporre di una proprietà di navigazione per ogni relazione di cui fa parte. Le proprietà di navigazione consentono di spostarsi e gestire le relazioni in entrambe le direzioni, restituendo un oggetto riferimento (se la molteplicità è una o zero-o-uno) o una raccolta (se la molteplicità è molti). È anche possibile scegliere di usare lo spostamento unidirezionale, nel qual caso si definisce la proprietà di navigazione solo su uno dei tipi che partecipano alla relazione e non su entrambi.

È consigliabile includere proprietà nel modello che eseguono il mapping a chiavi esterne nel database. Con le proprietà di chiave esterna incluse, è possibile creare o modificare una relazione cambiando il valore della chiave esterna in un oggetto dipendente. Questo tipo di associazione viene definito associazione di chiavi esterne. L'uso di chiavi esterne è ancora più essenziale quando si usano entità disconnesse. Si noti che quando si lavora con 1-a-1 o da 1 a 0.. 1 relazioni, non esiste una colonna chiave esterna separata, la proprietà chiave primaria funge da chiave esterna ed è sempre inclusa nel modello.

Quando le colonne chiave esterna non sono incluse nel modello, le informazioni sull'associazione vengono gestite come oggetto indipendente. Le relazioni vengono rilevate tramite riferimenti a oggetti anziché proprietà di chiave esterna. Questo tipo di associazione è denominato associazione indipendente. Il modo più comune per modificare un'associazione indipendente consiste nel modificare le proprietà di navigazione generate per ogni entità che partecipa all'associazione.

È possibile scegliere di utilizzare uno o entrambi i tipi di associazioni nel modello. Tuttavia, se si dispone di una relazione molti-a-molti pura connessa da una tabella di join che contiene solo chiavi esterne, Entity Framework userà un'associazione indipendente per gestire tale relazione molti-a-molti.   

L'immagine seguente mostra un modello concettuale creato con Entity Framework Designer. Il modello contiene due entità che partecipano alla relazione uno-a-molti. Entrambe le entità hanno proprietà di navigazione. Course è l'entità dipendente e ha la proprietà Chiave esterna DepartmentID definita.

Department and Course tables with navigation properties

Il frammento di codice seguente mostra lo stesso modello creato con Code First.

public class Course
{
  public int CourseID { get; set; }
  public string Title { get; set; }
  public int Credits { get; set; }
  public int DepartmentID { get; set; }
  public virtual Department Department { get; set; }
}

public class Department
{
   public Department()
   {
     this.Courses = new HashSet<Course>();
   }  
   public int DepartmentID { get; set; }
   public string Name { get; set; }
   public decimal Budget { get; set; }
   public DateTime StartDate { get; set; }
   public int? Administrator {get ; set; }
   public virtual ICollection<Course> Courses { get; set; }
}

Configurazione o mapping delle relazioni

Nella parte restante di questa pagina viene illustrato come accedere e modificare i dati usando le relazioni. Per informazioni sulla configurazione delle relazioni nel modello, vedere le pagine seguenti.

Creazione e modifica di relazioni

In un'associazione di chiavi esterne, quando si modifica la relazione, lo stato di un oggetto dipendente con uno EntityState.Unchanged stato cambia in EntityState.Modified. In una relazione indipendente, la modifica della relazione non aggiorna lo stato dell'oggetto dipendente.

Negli esempi seguenti viene illustrato come utilizzare le proprietà di chiave esterna e le proprietà di navigazione per associare gli oggetti correlati. Con le associazioni di chiavi esterne, è possibile usare un metodo per modificare, creare o modificare le relazioni. Con associazioni indipendenti, non è possibile utilizzare la proprietà di chiave esterna.

  • Assegnando un nuovo valore a una proprietà di chiave esterna, come nell'esempio seguente.

    course.DepartmentID = newCourse.DepartmentID;
    
  • Il codice seguente rimuove una relazione impostando la chiave esterna su Null. Si noti che la proprietà della chiave esterna deve essere nullable.

    course.DepartmentID = null;
    

    Nota

    Se il riferimento è nello stato aggiunto (in questo esempio, l'oggetto course), la proprietà di navigazione di riferimento non verrà sincronizzata con i valori chiave di un nuovo oggetto finché non viene chiamato SaveChanges. La sincronizzazione non si verifica in quanto il contesto dell'oggetto non contiene chiavi permanenti per gli oggetti aggiunti fino quando questi non vengono salvati. Se è necessario che i nuovi oggetti siano completamente sincronizzati non appena si imposta la relazione, utilizzare uno dei metodi seguenti.*

  • Assegnando un nuovo oggetto a una proprietà di navigazione. Il codice seguente crea una relazione tra un corso e un oggetto department. Se gli oggetti sono collegati al contesto, course viene aggiunto anche all'insieme department.Courses e la proprietà chiave esterna corrispondente nell'oggetto course viene impostata sul valore della proprietà chiave del reparto.

    course.Department = department;
    
  • Per eliminare la relazione, impostare la proprietà di navigazione su null. Se si usa Entity Framework basato su .NET 4.0, è necessario caricare la fine correlata prima di impostarla su Null. Ad esempio:

    context.Entry(course).Reference(c => c.Department).Load();
    course.Department = null;
    

    A partire da Entity Framework 5.0, basato su .NET 4.5, è possibile impostare la relazione su Null senza caricare la fine correlata. È anche possibile impostare il valore corrente su Null usando il metodo seguente.

    context.Entry(course).Reference(c => c.Department).CurrentValue = null;
    
  • Eliminando o aggiungendo un oggetto in una raccolta di entità. Ad esempio, è possibile aggiungere un oggetto di tipo Course all'insieme department.Courses . Questa operazione crea una relazione tra un determinato corso e un particolare departmentoggetto . Se gli oggetti sono collegati al contesto, il riferimento al reparto e la proprietà chiave esterna nell'oggetto course verranno impostati sull'oggetto appropriato department.

    department.Courses.Add(newCourse);
    
  • Utilizzando il ChangeRelationshipState metodo per modificare lo stato della relazione specificata tra due oggetti entità. Questo metodo viene usato più comunemente quando si usano applicazioni a più livelli e un'associazione indipendente (non può essere usata con un'associazione di chiavi esterne). Inoltre, per usare questo metodo è necessario selezionare ObjectContext, come illustrato nell'esempio seguente.
    Nell'esempio seguente esiste una relazione molti-a-molti tra istruttori e corsi. La chiamata al metodo e il ChangeRelationshipState passaggio del EntityState.Added parametro indica SchoolContext che è stata aggiunta una relazione tra i due oggetti:

    
    ((IObjectContextAdapter)context).ObjectContext.
      ObjectStateManager.
      ChangeRelationshipState(course, instructor, c => c.Instructor, EntityState.Added);
    

    Si noti che se si aggiorna (non solo aggiungendo) una relazione, è necessario eliminare la relazione precedente dopo aver aggiunto quello nuovo:

    ((IObjectContextAdapter)context).ObjectContext.
      ObjectStateManager.
      ChangeRelationshipState(course, oldInstructor, c => c.Instructor, EntityState.Deleted);
    

Sincronizzazione delle modifiche tra le chiavi esterne e le proprietà di navigazione

Quando si modifica la relazione degli oggetti collegati al contesto usando uno dei metodi descritti in precedenza, Entity Framework deve mantenere sincronizzate chiavi esterne, riferimenti e raccolte. Entity Framework gestisce automaticamente questa sincronizzazione (nota anche come correzione delle relazioni) per le entità POCO con proxy. Per altre informazioni, vedere Uso dei proxy.

Se si usano entità POCO senza proxy, è necessario assicurarsi che il metodo DetectChanges venga chiamato per sincronizzare gli oggetti correlati nel contesto. Si noti che le API seguenti attivano automaticamente una chiamata DetectChanges .

  • DbSet.Add
  • DbSet.AddRange
  • DbSet.Remove
  • DbSet.RemoveRange
  • DbSet.Find
  • DbSet.Local
  • DbContext.SaveChanges
  • DbSet.Attach
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries
  • Esecuzione di una query LINQ su un oggetto DbSet

In Entity Framework si usano in genere le proprietà di navigazione per caricare le entità correlate all'entità restituita dall'associazione definita. Per altre informazioni, vedere Caricamento di oggetti correlati.

Nota

In un'associazione di chiavi esterne, quando si carica un'entità finale correlata di un oggetto dipendente, l'oggetto correlato sarà caricato in base al valore della chiave esterna dell'oggetto dipendente attualmente in memoria:

    // Get the course where currently DepartmentID = 2.
    Course course = context.Courses.First(c => c.DepartmentID == 2);

    // Use DepartmentID foreign key property
    // to change the association.
    course.DepartmentID = 3;

    // Load the related Department where DepartmentID = 3
    context.Entry(course).Reference(c => c.Department).Load();

In un'associazione indipendente viene eseguita una query sull'entità finale correlata di un oggetto dipendente in base al valore della chiave esterna che è attualmente nel database. Tuttavia, se la relazione è stata modificata e la proprietà di riferimento dell'oggetto dipendente punta a un oggetto principale diverso caricato nel contesto dell'oggetto, Entity Framework tenterà di creare una relazione così come viene definita nel client.

Gestione della concorrenza

In entrambe le associazioni di chiave esterna e indipendenti, i controlli di concorrenza si basano sulle chiavi di entità e su altre proprietà di entità definite nel modello. Quando si usa Entity Framework Designer per creare un modello, impostare l'attributo ConcurrencyMode su fisso per specificare che la proprietà deve essere verificata per la concorrenza. Quando si usa Code First per definire un modello, usare l'annotazione ConcurrencyCheck sulle proprietà da controllare per la concorrenza. Quando si usa Code First, è anche possibile usare l'annotazione TimeStamp per specificare che la proprietà deve essere verificata per la concorrenza. È possibile avere una sola proprietà timestamp in una determinata classe. Code First esegue il mapping di questa proprietà a un campo non nullable nel database.

È consigliabile usare sempre l'associazione di chiavi esterne quando si usano entità che partecipano al controllo e alla risoluzione della concorrenza.

Per altre informazioni, vedere Gestione dei conflitti di concorrenza.

Uso di chiavi sovrapposte

Le chiavi sovrapposte sono chiavi composte in cui alcune proprietà nella chiave fanno parte anche di un'altra chiave nell'entità. Non è possibile disporre di una chiave sovrapposta in un'associazione indipendente. Per modificare un'associazione di chiavi esterne che include chiavi sovrapposte, si consiglia di modificare i valori della chiave esterna anziché utilizzare i riferimenti a un oggetto.