Rilevamento esplicito delle entità
Ogni istanza di DbContext tiene traccia delle modifiche apportate alle entità. Queste entità rilevate a loro volta determinano le modifiche apportate al database quando viene chiamato SaveChanges.
Il rilevamento delle modifiche di Entity Framework Core (EF Core) funziona meglio quando la stessa DbContext istanza viene usata per eseguire query per le entità e aggiornarle chiamando SaveChanges. Ciò dipende dal fatto che EF Core rileva automaticamente lo stato delle entità sottoposte a query e quindi rileva le eventuali modifiche apportate a queste entità quando viene chiamato SaveChanges. Questo approccio è trattato in Rilevamento modifiche in EF Core.
Suggerimento
Questo documento presuppone che gli stati dell'entità e le nozioni di base del rilevamento delle modifiche di EF Core siano compresi. Per altre informazioni su questi argomenti, vedere Rilevamento modifiche in EF Core.
Suggerimento
È possibile eseguire ed eseguire il debug in tutto il codice di questo documento scaricando il codice di esempio da GitHub.
Suggerimento
Per semplicità, questo documento usa e fa riferimento a metodi sincroni, ad esempio SaveChanges, anziché i relativi equivalenti asincroni, ad esempio SaveChangesAsync. La chiamata e l'attesa del metodo asincrono possono essere sostituite se non diversamente specificato.
Introduzione
Le entità possono essere "collegate" in modo esplicito a un DbContext oggetto in modo che il contesto possa quindi tenere traccia di tali entità. Ciò è particolarmente utile quando:
- Creazione di nuove entità che verranno inserite nel database.
- Ricollegare le entità disconnesse sottoposte a query in precedenza da un'istanza dbContext diversa .
Il primo di questi sarà necessario per la maggior parte delle applicazioni e viene gestito principalmente dai DbContext.Add metodi .
Il secondo è necessario solo per le applicazioni che modificano le entità o le relative relazioni mentre le entità non vengono rilevate. Ad esempio, un'applicazione Web può inviare entità al client Web in cui l'utente apporta modifiche e invia di nuovo le entità. Queste entità vengono definite "disconnesse" perché sono state originariamente sottoposte a query da un DbContext, ma sono state quindi disconnesse da tale contesto quando vengono inviate al client.
L'applicazione Web deve ora ricollegare queste entità in modo che vengano nuovamente rilevate e indicare le modifiche apportate in modo che SaveChanges possano apportare aggiornamenti appropriati al database. Questa operazione viene gestita principalmente dai DbContext.Attach metodi e DbContext.Update .
Suggerimento
Il collegamento di entità alla stessa istanza dbContext da cui è stata eseguita una query non deve essere in genere necessario. Non eseguire regolarmente una query senza rilevamento e quindi allegare le entità restituite allo stesso contesto. Questa operazione sarà più lenta rispetto all'uso di una query di rilevamento e potrebbe anche causare problemi come i valori delle proprietà shadow mancanti, rendendo più difficile ottenere il giusto risultato.
Valori di chiave generati e espliciti
Per impostazione predefinita, le proprietà chiave integer e GUID sono configurate per l'uso di valori di chiave generati automaticamente. Questo ha un vantaggio importante per il rilevamento delle modifiche: un valore di chiave unset indica che l'entità è "new". Per "new" si intende che non è ancora stato inserito nel database.
Nelle sezioni seguenti vengono usati due modelli. Il primo è configurato per non usare i valori di chiave generati:
public class Blog
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
}
I valori di chiave non generati (ovvero impostati in modo esplicito) vengono visualizzati per primi in ogni esempio perché tutto è molto esplicito e facile da seguire. Viene quindi seguito da un esempio in cui vengono usati i valori di chiave generati:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
}
Si noti che le proprietà chiave in questo modello non richiedono alcuna configurazione aggiuntiva perché l'uso di valori di chiave generati è l'impostazione predefinita per le chiavi integer semplici.
Inserimento di nuove entità
Valori di chiave espliciti
È necessario tenere traccia di un'entità Added
nello stato da inserire da SaveChanges. Le entità vengono in genere inserite nello stato Added chiamando uno dei DbContext.Addmetodi , DbContext.AddRange, DbContext.AddAsyncDbContext.AddRangeAsync, o equivalenti in DbSet<TEntity>.
Suggerimento
Tutti questi metodi funzionano nello stesso modo nel contesto del rilevamento delle modifiche. Per altre informazioni, vedere Funzionalità aggiuntive Rilevamento modifiche.
Ad esempio, per iniziare a tenere traccia di un nuovo blog:
context.Add(
new Blog { Id = 1, Name = ".NET Blog", });
Esaminando la visualizzazione di debug dello strumento di rilevamento modifiche seguente, questa chiamata mostra che il contesto sta monitorando la nuova entità nello Added
stato:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: []
Tuttavia, i metodi Add non funzionano solo su una singola entità. Iniziano effettivamente a tenere traccia di un intero grafico di entità correlate, inserendoli tutti allo Added
stato. Ad esempio, per inserire un nuovo blog e i nuovi post associati:
context.Add(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Il contesto ora monitora tutte queste entità come Added
:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Added
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Si noti che sono stati impostati valori espliciti per le Id
proprietà chiave negli esempi precedenti. Il motivo è che il modello qui è stato configurato per l'uso esplicito dei valori di chiave impostati in modo esplicito, anziché per i valori di chiave generati automaticamente. Quando non si usano chiavi generate, le proprietà della chiave devono essere impostate in modo esplicito prima di chiamare Add
. Questi valori di chiave vengono quindi inseriti quando viene chiamato SaveChanges. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);
-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);
Tutte queste entità vengono rilevate nello stato dopo il Unchanged
completamento di SaveChanges, poiché queste entità sono ora presenti nel database:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Valori di chiave generati
Come accennato in precedenza, le proprietà della chiave integer e GUID sono configurate per l'uso dei valori di chiave generati automaticamente per impostazione predefinita. Ciò significa che l'applicazione non deve impostare alcun valore di chiave in modo esplicito. Ad esempio, per inserire un nuovo blog e inserire tutti i post con valori di chiave generati:
context.Add(
new Blog
{
Name = ".NET Blog",
Posts =
{
new Post
{
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Come per i valori di chiave espliciti, il contesto ora monitora tutte queste entità come Added
:
Blog {Id: -2147482644} Added
Id: -2147482644 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
Id: -2147482637 PK Temporary
BlogId: -2147482644 FK Temporary
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
Id: -2147482636 PK Temporary
BlogId: -2147482644 FK Temporary
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: -2147482644}
Si noti in questo caso che i valori di chiave temporanei sono stati generati per ogni entità. Questi valori vengono usati da EF Core fino a quando non viene chiamato SaveChanges, in cui i valori di chiave reali vengono letti dal database. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Al termine di SaveChanges, tutte le entità sono state aggiornate con i relativi valori di chiave reali e vengono rilevate nello Unchanged
stato poiché ora corrispondono allo stato nel database:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Si tratta esattamente dello stesso stato finale dell'esempio precedente che usa valori di chiave espliciti.
Suggerimento
È comunque possibile impostare un valore di chiave esplicito anche quando si usano valori di chiave generati. EF Core tenterà quindi di inserire usando questo valore di chiave. Alcune configurazioni di database, tra cui SQL Server con colonne Identity, non supportano tali inserimenti e generano un'eccezione (vedere questi documenti per una soluzione alternativa).
Collegamento di entità esistenti
Valori di chiave espliciti
Le entità restituite dalle query vengono rilevate nello Unchanged
stato . Lo Unchanged
stato indica che l'entità non è stata modificata dopo che è stata eseguita una query. Un'entità disconnessa, forse restituita da un client Web in una richiesta HTTP, può essere inserita in questo stato usando DbContext.Attach, DbContext.AttachRangeo i metodi equivalenti in DbSet<TEntity>. Ad esempio, per iniziare a tenere traccia di un blog esistente:
context.Attach(
new Blog { Id = 1, Name = ".NET Blog", });
Nota
Gli esempi seguenti illustrano come creare entità in modo esplicito con new
per semplicità. In genere le istanze di entità provengono da un'altra origine, ad esempio essere deserializzate da un client o create da dati in un post HTTP.
Esaminando la visualizzazione di debug dello strumento di rilevamento delle modifiche seguente, questa chiamata mostra che l'entità viene rilevata nello Unchanged
stato:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: []
Proprio come Add
, Attach
imposta effettivamente un intero grafico di entità connesse allo Unchanged
stato. Ad esempio, per allegare un blog esistente e i post esistenti associati:
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Il contesto ora monitora tutte queste entità come Unchanged
:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
La chiamata a SaveChanges a questo punto non avrà alcun effetto. Tutte le entità sono contrassegnate come Unchanged
, quindi non è necessario aggiornare nel database.
Valori di chiave generati
Come accennato in precedenza, le proprietà della chiave integer e GUID sono configurate per l'uso dei valori di chiave generati automaticamente per impostazione predefinita. Questo è un vantaggio importante quando si lavora con entità disconnesse: un valore di chiave non impostato indica che l'entità non è ancora stata inserita nel database. In questo modo, lo strumento di rilevamento delle modifiche può rilevare automaticamente nuove entità e inserirle nello Added
stato . Ad esempio, è consigliabile allegare questo grafico di un blog e di un post:
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
},
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
},
}
});
Il blog ha un valore chiave pari a 1, a indicare che esiste già nel database. Due dei post hanno anche valori chiave impostati, ma il terzo non lo è. EF Core vedrà questo valore di chiave come 0, l'impostazione predefinita CLR per un numero intero. Di conseguenza, EF Core contrassegna la nuova entità come Added
anziché Unchanged
:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
Id: -2147482636 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 includes many enhancements, including single file a...'
Title: 'Announcing .NET 5.0'
Blog: {Id: 1}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
La chiamata a SaveChanges a questo punto non esegue alcuna operazione con le Unchanged
entità, ma inserisce la nuova entità nel database. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Il punto importante da notare è che, con i valori di chiave generati, EF Core è in grado di distinguere automaticamente le nuove entità esistenti in un grafico disconnesso. In breve, quando si usano chiavi generate, EF Core inserirà sempre un'entità quando tale entità non ha un valore di chiave impostato.
Aggiornamento di entità esistenti
Valori di chiave espliciti
DbContext.Update, DbContext.UpdateRangee i metodi equivalenti su DbSet<TEntity> si comportano esattamente come i Attach
metodi descritti in precedenza, ad eccezione del fatto che le entità vengono inserite invece Modified
dello Unchanged
stato . Ad esempio, per iniziare a tenere traccia di un blog esistente come Modified
:
context.Update(
new Blog { Id = 1, Name = ".NET Blog", });
Esaminando la visualizzazione di debug dello strumento di rilevamento modifiche seguente, questa chiamata mostra che il contesto sta monitorando questa entità nello Modified
stato:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: []
Proprio come con Add
e Attach
, Update
contrassegna effettivamente un intero grafico di entità correlate come Modified
. Ad esempio, per allegare un blog esistente e i post esistenti associati come Modified
:
context.Update(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Il contesto ora monitora tutte queste entità come Modified
:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
Id: 1 PK
BlogId: 1 FK Modified Originally <null>
Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
Title: 'Announcing the Release of EF Core 5.0' Modified
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK Modified Originally <null>
Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
Title: 'Announcing F# 5' Modified
Blog: {Id: 1}
La chiamata a SaveChanges a questo punto causerà l'invio degli aggiornamenti al database per tutte queste entità. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
Valori di chiave generati
Come per Attach
, i valori di chiave generati hanno lo stesso vantaggio principale per Update
: un valore di chiave non impostato indica che l'entità è nuova e non è ancora stata inserita nel database. Come con Attach
, ciò consente a DbContext di rilevare automaticamente nuove entità e inserirle nello Added
stato . Si consideri ad esempio la possibilità di chiamare Update
con questo grafico di un blog e di un post:
context.Update(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
},
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
},
}
});
Come nell'esempio Attach
, il post senza valore chiave viene rilevato come nuovo e impostato sullo Added
stato. Le altre entità sono contrassegnate come Modified
:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
Id: -2147482633 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 includes many enhancements, including single file a...'
Title: 'Announcing .NET 5.0'
Blog: {Id: 1}
Post {Id: 1} Modified
Id: 1 PK
BlogId: 1 FK Modified Originally <null>
Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
Title: 'Announcing the Release of EF Core 5.0' Modified
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK Modified Originally <null>
Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
Title: 'Announcing F# 5' Modified
Blog: {Id: 1}
La chiamata SaveChanges
a questo punto causerà l'invio degli aggiornamenti al database per tutte le entità esistenti, mentre viene inserita la nuova entità. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Si tratta di un modo molto semplice per generare aggiornamenti e inserimenti da un grafico disconnesso. Tuttavia, comporta l'invio di aggiornamenti o inserimenti al database per ogni proprietà di ogni entità rilevata, anche quando alcuni valori delle proprietà potrebbero non essere stati modificati. Non essere troppo spaventata da questo; per molte applicazioni con piccoli grafici, questo può essere un modo semplice e pragmatico di generare aggiornamenti. Detto questo, altri modelli più complessi possono talvolta comportare aggiornamenti più efficienti, come descritto in Risoluzione delle identità in EF Core.
Eliminazione di entità esistenti
Affinché un'entità venga eliminata da SaveChanges, deve essere rilevata nello Deleted
stato . Le entità vengono in genere inserite nello Deleted
stato chiamando uno di DbContext.Remove, DbContext.RemoveRangeo i metodi equivalenti in DbSet<TEntity>. Ad esempio, per contrassegnare un post esistente come Deleted
:
context.Remove(
new Post { Id = 2 });
Esaminando la visualizzazione di debug dello strumento di rilevamento delle modifiche dopo questa chiamata, il contesto sta monitorando l'entità Deleted
nello stato:
Post {Id: 2} Deleted
Id: 2 PK
BlogId: <null> FK
Content: <null>
Title: <null>
Blog: <null>
Questa entità verrà eliminata quando viene chiamato SaveChanges. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Al termine di SaveChanges, l'entità eliminata viene disconnessa da DbContext perché non esiste più nel database. La vista di debug è quindi vuota perché non viene rilevata alcuna entità.
Eliminazione di entità dipendenti/figlio
L'eliminazione di entità dipendenti/figlio da un grafico è più semplice rispetto all'eliminazione di entità principale/padre. Per altre informazioni, vedere la sezione successiva e La modifica di chiavi esterne e spostamenti .
È insolito chiamare Remove
su un'entità creata con new
. A differenza di Add
, Attach
e Update
, non è raro chiamare Remove
su un'entità che non è già rilevata nello Unchanged
stato o Modified
. È invece tipico tenere traccia di una singola entità o grafo di entità correlate e quindi chiamare Remove
sulle entità che devono essere eliminate. Questo grafico delle entità rilevate viene in genere creato da:
- Esecuzione di una query per le entità
- Uso dei
Attach
metodi oUpdate
in un grafico di entità disconnesse, come descritto nelle sezioni precedenti.
Ad esempio, il codice nella sezione precedente è più probabile ottenere un post da un client e quindi eseguire un'operazione simile alla seguente:
context.Attach(post);
context.Remove(post);
Questo comportamento corrisponde esattamente a quello dell'esempio precedente, poiché la chiamata Remove
a un'entità non rilevata fa sì che venga prima collegata e quindi contrassegnata come Deleted
.
In esempi più realistici, viene prima collegato un grafico di entità e quindi alcune di queste entità vengono contrassegnate come eliminate. Ad esempio:
// Attach a blog and associated posts
context.Attach(blog);
// Mark one post as Deleted
context.Remove(blog.Posts[1]);
Tutte le entità vengono contrassegnate come Unchanged
, ad eccezione di quella in cui Remove
è stato chiamato:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Questa entità verrà eliminata quando viene chiamato SaveChanges. Ad esempio, quando si usa SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Al termine di SaveChanges, l'entità eliminata viene disconnessa da DbContext perché non esiste più nel database. Altre entità rimangono nello Unchanged
stato :
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Eliminazione di entità principali/entità padre
Ogni relazione che connette due tipi di entità ha un'entità o un endpoint padre e una fine dipendente o figlio. L'entità dipendente/figlio è quella con la proprietà di chiave esterna. In una relazione uno-a-molti, l'entità/padre si trova sul lato "uno" e il dipendente/figlio si trova sul lato "molti". Per altre informazioni, vedere Relazioni .
Negli esempi precedenti è stato eliminato un post, che è un'entità dipendente/figlio nella relazione uno-a-molti nei post di blog. Questa operazione è relativamente semplice perché la rimozione di un'entità dipendente/figlio non ha alcun impatto su altre entità. D'altra parte, l'eliminazione di un'entità principale/padre deve influire anche su qualsiasi entità dipendente/figlio. In caso contrario, lasciare un valore di chiave esterna che fa riferimento a un valore di chiave primaria che non esiste più. Si tratta di uno stato del modello non valido e genera un errore di vincolo referenziale nella maggior parte dei database.
Questo stato del modello non valido può essere gestito in due modi:
- Impostazione dei valori FK su Null. Ciò indica che i dipendenti/figli non sono più correlati a un'entità o a un elemento padre. Si tratta dell'impostazione predefinita per le relazioni facoltative in cui la chiave esterna deve essere nullable. L'impostazione di FK su null non è valida per le relazioni necessarie, in cui la chiave esterna è in genere non nullable.
- Eliminazione dei dipendenti/figli. Si tratta dell'impostazione predefinita per le relazioni obbligatorie ed è valida anche per le relazioni facoltative.
Per informazioni dettagliate sul rilevamento delle modifiche e sulle relazioni, vedere Modifica di chiavi esterne e spostamenti .
Relazioni facoltative
La Post.BlogId
proprietà della chiave esterna è nullable nel modello in uso. Ciò significa che la relazione è facoltativa e quindi il comportamento predefinito di EF Core consiste nell'impostare BlogId
le proprietà della chiave esterna su Null quando il blog viene eliminato. Ad esempio:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
Esaminando la visualizzazione di debug di Rilevamento modifiche dopo la chiamata a Remove
viene mostrato che, come previsto, il blog è ora contrassegnato come Deleted
:
Blog {Id: 1} Deleted
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
Id: 1 PK
BlogId: <null> FK Modified Originally 1
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: <null>
Post {Id: 2} Modified
Id: 2 PK
BlogId: <null> FK Modified Originally 1
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Più interessante, tutti i post correlati sono ora contrassegnati come Modified
. Ciò è dovuto al fatto che la proprietà della chiave esterna in ogni entità è stata impostata su Null. La chiamata a SaveChanges aggiorna il valore della chiave esterna per ogni post su Null nel database, prima di eliminare il blog:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();
Al termine di SaveChanges, l'entità eliminata viene disconnessa da DbContext perché non esiste più nel database. Altre entità sono ora contrassegnate come Unchanged
con valori di chiave esterna Null, che corrispondono allo stato del database:
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: <null> FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: <null>
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: <null> FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Relazioni obbligatorie
Se la Post.BlogId
proprietà di chiave esterna non è nullable, la relazione tra blog e post diventa "obbligatoria". In questo caso, EF Core eliminerà per impostazione predefinita le entità dipendenti/figlio quando l'entità principale/padre viene eliminata. Ad esempio, l'eliminazione di un blog con post correlati come nell'esempio precedente:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
Esaminando la visualizzazione di debug di Rilevamento modifiche dopo la chiamata a Remove
viene mostrato che, come previsto, il blog viene contrassegnato di nuovo come Deleted
:
Blog {Id: 1} Deleted
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Più interessante in questo caso è che anche tutti i post correlati sono stati contrassegnati come Deleted
. La chiamata a SaveChanges causa l'eliminazione del blog e di tutti i post correlati dal database:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;
Al termine di SaveChanges, tutte le entità eliminate vengono scollegate da DbContext perché non esistono più nel database. L'output dalla visualizzazione di debug è quindi vuoto.
Nota
Questo documento graffia solo la superficie sull'uso delle relazioni in EF Core. Per altre informazioni sulla modellazione delle relazioni e sulla modifica di chiavi esterne e spostamenti, vedere Relazioni per altre informazioni sull'aggiornamento o l'eliminazione di entità dipendenti/figlio durante la chiamata a SaveChanges.
Rilevamento personalizzato con TrackGraph
ChangeTracker.TrackGraph funziona come Add
, Attach
e Update
ad eccezione del fatto che genera un callback per ogni istanza di entità prima di monitorarlo. In questo modo è possibile usare la logica personalizzata per determinare come tenere traccia delle singole entità in un grafico.
Si consideri ad esempio la regola usata da EF Core per tenere traccia delle entità con valori di chiave generati: se il valore della chiave è zero, l'entità è nuova e deve essere inserita. Estendere questa regola per indicare se il valore della chiave è negativo, l'entità deve essere eliminata. In questo modo è possibile modificare i valori della chiave primaria nelle entità di un grafo disconnesso per contrassegnare le entità eliminate:
blog.Posts.Add(
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
}
);
var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;
Questo grafico disconnesso può quindi essere rilevato usando TrackGraph:
public static async Task UpdateBlog(Blog blog)
{
using var context = new BlogsContext();
context.ChangeTracker.TrackGraph(
blog, node =>
{
var propertyEntry = node.Entry.Property("Id");
var keyValue = (int)propertyEntry.CurrentValue;
if (keyValue == 0)
{
node.Entry.State = EntityState.Added;
}
else if (keyValue < 0)
{
propertyEntry.CurrentValue = -keyValue;
node.Entry.State = EntityState.Deleted;
}
else
{
node.Entry.State = EntityState.Modified;
}
Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
});
await context.SaveChangesAsync();
}
Per ogni entità nel grafico, il codice precedente controlla il valore della chiave primaria prima di tenere traccia dell'entità. Per i valori di chiave unset (zero), il codice esegue normalmente le operazioni eseguite da EF Core. Ovvero, se la chiave non è impostata, l'entità viene contrassegnata come Added
. Se la chiave è impostata e il valore non è negativo, l'entità viene contrassegnata come Modified
. Tuttavia, se viene trovato un valore di chiave negativo, viene ripristinato il valore reale, non negativo e l'entità viene rilevata come Deleted
.
L'output dell'esecuzione di questo codice è:
Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added
Nota
Per semplicità, questo codice presuppone che ogni entità abbia una proprietà chiave primaria integer denominata Id
. Questa operazione può essere codificata in una classe o un'interfaccia di base astratta. In alternativa, la proprietà o le proprietà della chiave primaria possono essere ottenute dai IEntityType metadati in modo che questo codice funzioni con qualsiasi tipo di entità.
TrackGraph dispone di due overload. Nell'overload semplice usato in precedenza EF Core determina quando interrompere l'attraversamento del grafico. In particolare, interrompe la visita di nuove entità correlate da una determinata entità quando tale entità è già tracciata o quando il callback non avvia il rilevamento dell'entità.
L'overload avanzato, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), ha un callback che restituisce un valore bool. Se il callback restituisce false, l'attraversamento grafico si arresta, altrimenti continua. Prestare attenzione a evitare cicli infiniti quando si usa questo overload.
L'overload avanzato consente anche di fornire lo stato a TrackGraph e questo stato viene quindi passato a ogni callback.