sto lottando da un po 'per capire come EF carica / aggiorna le entità. Prima di tutto voglio spiegare di cosa parla la mia app (WPF). Sto sviluppando un'applicazione in cui gli utenti possono memorizzare elementi di Todo in categorie, queste categorie sono predefinite dall'applicazione. Ogni utente può leggere tutti gli articoli ma può solo cancellare / aggiornare i propri articoli. È un sistema multiutente, significa che l'applicazione è in esecuzione più volte nella rete che accede allo stesso database del server SQL. Quando un utente aggiunge / elimina / aggiorna gli elementi, l'interfaccia utente di tutte le altre app in esecuzione deve essere aggiornata.
Il mio modello assomiglia a questo:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public List<Todo> Todos { get; set; }
}
public class Todo
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime LastUpdate { get; set; }
public string Owner { get; set; }
public Category Category { get; set; }
public List<Info> Infos { get; set; }
}
public class Info
{
public int Id { get; set; }
public string Value { get; set; }
public Todo Todo { get; set; }
}
Sto facendo il caricamento iniziale come questo, che funziona bene:
Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Select(t => t.Infos)).FirstOrDefault();
Ora stavo cercando di caricare solo i Todos che provengono dall'attuale utente, quindi ho provato questo:
Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Where(t => t.Owner == Settings.User).Select(t => t.Infos)).FirstOrDefault();
Questo non funziona perché non è possibile filtrare all'interno di include, quindi ho provato questo:
var cat = Context.dbsCategories.Where(c => c.Id == id).FirstOrDefault();
Context.dbsTodos.Where(t => t.Category.Id == cat.Id && t.Owner == Settings.User).Include(t=>t.Infos);
Dopo aver eseguito la seconda riga dove cerco gli oggetti da Todo, questi articoli sono stati aggiunti automaticamente alla collezione Todos di cat. Perché? Mi sarei aspettato di doverli aggiungere manualmente alla collezione Todos di cat. Solo per la mia comprensione che cosa sta facendo EF qui esattamente?
Ora per il mio problema principale -> la sincronizzazione dei dati tra database e client. Sto usando un contesto di lunga durata che dura fintanto che l'applicazione è in esecuzione per salvare le modifiche al database che sono fatte su oggetti di proprietà. L'utente non ha la possibilità di manipolare / cancellare dati da altri utenti, questo è garantito dall'interfaccia utente. Per sincronizzare i dati, costruisco questo metodo di sincronizzazione che verrà eseguito ogni 10 secondi, in questo momento è triggere manualmente.
Questo è il mio codice di sincronizzazione, che sincronizza solo gli elementi sul client che non gli appartengono.
private async Task Synchronize()
{
using (var ctx = new Context())
{
var database = ctx.dbsTodos().Where(x => x.Owner != Settings.User).Select(t => t.Infos).AsNoTracking();
var loaded = Context.dbsTodos.Local.Where(x => x.Owner != Settings.User);
//In local context but not in database anymore -> Detachen
foreach (var detach in loaded.Except(database, new TodoIdComparer()).ToList())
{
Context.ObjectContext.Detach(detach);
Log.Debug(this, $"Item {detach} detached");
}
//In database and local context -> Check Timestamp -> Update
foreach (var update in loaded.Intersect(database, new TodoIdTimeStampComparer()))
{
await Context.Entry(update).ReloadAsync();
Log.Debug(this, $"Item {update} updated");
}
//In database but not in local context -> Attach
foreach (var attach in database.ToList().Except(loaded, new TodoIdComparer()))
{
Context.dbsTodos().Attach(attach);
Log.Debug(this, $"Item {attach} attached");
}
}
}
Sto seguendo problemi / problemi di origine sconosciuta con esso: staccare gli elementi eliminati sembra funzionare, in questo momento non sono sicuro se solo gli oggetti di Todo sono staccati o anche le informazioni.
Aggiornamento articoli funziona solo per TodoItem stesso, non ricaricando le informazioni all'interno? Come posso ricaricare l'intera entità con tutte le sue relazioni? Sono grato per ogni aiuto su questo, anche se stai dicendo che è tutto sbagliato quello che sto facendo qui!
Allegare nuovi articoli e informazioni non funziona finora? Cosa sto facendo di sbagliato qui?
È questo l'approccio giusto per sincronizzare i dati tra client e database? Cosa sto facendo di sbagliato qui? Esiste un tutorial "How to Sync"? Non ho trovato nulla di utile finora?
Grazie!
A me, ti piace deviare dalle convenzioni sul codice framework dell'entità , vero?
Le relazioni tra le tue tabelle sono Elenchi, invece di ICollections, non sono dichiarati virtuali e hai dimenticato di dichiarare la chiave esterna
Esiste una relazione uno-a-molti tra Todo e Categoria: ogni Todo
appartiene a una sola Category
(usando una chiave straniera), ogni Categoria ha zero o più Todos.
Scegli di dare una proprietà a Categoria List<Todo> Todos {get; set;}
. Sei sicuro che la category.Todos[4]
ha un significato definito? E cosa significherebbe `category.Todos.Insert (4, new Todo ())?
Meglio attenersi a un'interfaccia in cui non è possibile utilizzare funzioni che non hanno un significato corretto nel database: utilizzare `ICollection Todos {get; impostato;}. In questo modo avrai accesso solo alle funzioni che Entity Framework può tradurre in SQL.
Inoltre, una query sarà probabilmente più veloce: offrite al framework di entità la possibilità di interrogare i dati nel modo più efficiente, invece di forzarli a mettere il risultato in una lista.
Nel framework di entità le colonne di una tabella sono rappresentate da proprietà non virtuali; le proprietà virtuali rappresentano le relazioni tra le tabelle (uno-a-molti, molti-a-molti)
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
... // other properties
// every Category has zero or more Todos (one-to-many)
public virtual ICollection<Todo> Todos { get; set; }
}
public class Todo
{
public int Id { get; set; }
public string Content { get; set; }
... // other properties
// every Todo belongs to exactly one Category, using foreign key
public int CategoryId { get; set }
public virtual Category Category { get; set; }
// every Category has zero or more Infos:
public virtual ICollection<Info> Infos { get; set; }
}
Probabilmente dovrai indovinare Info ora:
public class Info
{
public int Id { get; set; }
public string Value { get; set; }
... // other properties
// every info belongs to exactly one Todo, using foreign key
public int TodoId {get; set;}
public virtual Todo Todo { get; set; }
}
Tre miglioramenti principali: ICollection, virtual, foreign key definition: sono colonne reali nelle tue tabelle.
Una delle parti più lente di una query del database è il trasporto dei dati selezionati dal sistema di gestione dei database al processo locale. Quindi è consigliabile limitare la quantità di dati trasportati.
Supponiamo che la Categoria con Id 4 abbia mille Todos. Ogni Todo avrà una chiave esterna per la categoria a cui appartiene con un valore 4. Quindi questo stesso valore 4 verrà trascritto 1001 volte. Che spreco!
Nel framework di entità utilizzare Seleziona invece di Includi per eseguire query sui dati e selezionare solo le proprietà che si prevede di utilizzare. Utilizzare solo Includi se si prevede di aggiornare i dati selezionati.
Dammi tutte le Categorie che ... con i loro Todos che ...
var results = dbContext.Categories
.Where(category => ...)
.Select(category => new
{
// only select properties that you plan to use
Id = category.Id,
Name = category.Name,
...
Todos = category.Todos
.Where(todo => ...) // only if you don't want all Todos
.Select(todo => new
{
// again, select only the properties you'll plan to use
Id = todo.Id,
...
// not needed, you know the value: CategoryId = todo.CategoryId
// only if you also want some infos:
Infos = todo.Infos
.Select(info => ....) // you know the drill by now
.ToList(),
})
.ToList(),
});
Un altro problema è che mantieni il tuo DbContext
aperto per un po 'di tempo. Questo non è il significato di un dbContext. Se il tuo database cambia tra la tua query e il tuo aggiornamento, avrai dei problemi. Riesco a malapena a immaginare di interrogare così tanti dati che è necessario ottimizzarli mantenendo vivo il tuo dbContext. Anche se si esegue una query su molti dati, la visualizzazione di questa enorme quantità di dati sarebbe il collo di bottiglia, non la query del database.
È meglio recuperare i dati una volta, disporre di DbContext e, quando si aggiornano i dati di recupero, aggiornare le proprietà modificate e SaveChanges.
recupera dati:
RepositoryCategory FetchCategory(int categoryId)
{
using (var dbContext = new MyDbContext())
{
return dbContext.Categories.Where(category => category.Id == categoryId)
.Select(category => new RepositoryCategory
{
... // see above
})
.FirstOrDefault();
}
}
Sì, avrai bisogno di una categoria di RepositoryCategory
classe extra per questo. Il vantaggio è che nascondi che hai recuperato i tuoi dati da un database. Il tuo codice cambierebbe difficilmente se recuperi i tuoi dati da un file CSV o da Internet. Questo è il modo migliore testabile e anche il modo più gestibile: se la tabella delle categorie nel tuo database cambia, gli utenti della tua RepositoryCategory non se ne accorgeranno.
Prendi in considerazione la creazione di uno spazio dei nomi speciale per i dati recuperati dal tuo database. In questo modo è possibile chiamare la categoria di categoria recuperata, invece di RepositoryCategory. Puoi persino nascondere meglio da dove hai prelevato i tuoi dati.
Hai scritto:
Ora stavo cercando di caricare solo i Todos che provengono dall'attuale utente
Dopo i precedenti miglioramenti, sarà facile:
string owner = Settings.User; // or something similar
var result = dbContext.Todos.Where(todo => todo.Owner == owner)
.Select(todo => new
{
// properties you need
})