Contesto : sto realizzando un'app per la gestione delle collezioni per i musei. Ogni entità ha una proprietà del Museo per legare quell'entità al museo appropriato. Un utente di un dato museo dovrebbe essere in grado di mantenere un lessico attraverso l'app per cose come Genres. Allo stesso modo, i musei possono mantenere un elenco di artisti. Le versioni annacquate delle classi pertinenti e uno schema del database finora sono di seguito:
public class Museum
{
public Museum()
{
Artists = new List<Artist>();
Genres = new List<Genre>();
}
public int Id { get; set; }
public string Name { get; set; }
public List<Artist> Artists { get; set; }
public List<Genre> Genres { get; set; }
}
public class Artist
{
public int Id { get; set; }
public string Name { get; set; }
public Museum Museum { get; set; }
public int MuseumId { get; set; }
}
public class Genre
{
public int Id { get; set; }
public string Name { get; set; }
public Museum Museum { get; set; }
public int MuseumId { get; set; }
}
Problema : un artista crea opere in una varietà di generi, quindi un artista ha molti generi. Allo stesso modo, un genere è usato da molti artisti. Quindi qui ho bisogno di una relazione molti-a-molti che posso raggiungere attraverso EFCore definendo esplicitamente un'entità che unisce: ArtistGenre. Codice aggiornato qui sotto:
public class Artist
{
public Artist()
{
ArtistGenres = new List<ArtistGenre>();
}
public int Id { get; set; }
public string Name { get; set; }
public Museum Museum { get; set; }
public int MuseumId { get; set; }
public List<ArtistGenre> ArtistGenres { get; set; }
}
public class Genre
{
public Genre()
{
ArtistGenres = new List<ArtistGenre>();
}
public int Id { get; set; }
public string Name { get; set; }
public Museum Museum { get; set; }
public int MuseumId { get; set; }
public List<ArtistGenre> ArtistGenres { get; set; }
}
public class ArtistGenre
{
public Artist Artist { get; set; }
public int ArtistId { get; set; }
public Genre Genre { get; set; }
public int GenreId { get; set; }
}
E l'obbligatorio: modelBuilder.Entity<ArtistGenre>().HasKey(a => new { a.ArtistId, a.GenreId });
in OnModelCreating nel mio DbContext. Ma questo si traduce in una situazione in cui vi sono più percorsi di eliminazione a cascata per la stessa entità:
L'introduzione del vincolo FOREIGN KEY "FK_ArtistGenre_Genres_GenreId" sulla tabella "ArtistGenre" potrebbe causare cicli o più percorsi a cascata. Specificare ON DELETE NO ACTION o ON UPDATE NO ACTION o modificare altri vincoli FOREIGN KEY. Impossibile creare vincoli o indici. Vedi errori precedenti.
C'è un trucco per farlo funzionare? Ho progettato questo sbagliato? Alla fine, mi piacerebbe essere in grado di cancellare un museo e avere tutti i generi e artisti (e artisti) correlati cancellati automaticamente.
AGGIORNARE
Ho sperimentato diverse configurazioni e ho scoperto che se rimuovo le eliminazioni a cascata da ArtistGenre in questo modo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ArtistGenre>()
.HasOne(a => a.Artist)
.WithMany(m => m.ArtistGenres)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ArtistGenre>()
.HasOne(a => a.Genre)
.WithMany(g => g.ArtistGenres)
.OnDelete(DeleteBehavior.Restrict);
}
Quindi sono in grado di aggiornare il database con la tabella di giunzione, ma non ottengo l'effetto di eliminazione a cascata desiderato. Se aggiungo una proprietà Musuem e MuseumId a ArtistGenre in questo modo:
public class ArtistGenre
{
public Museum Museum { get; set; }
public int MuseumId { get; set; }
public Artist Artist { get; set; }
public int ArtistId { get; set; }
public Genre Genre { get; set; }
public int GenreId { get; set; }
}
Posso cancellare un museo e avere tutte le entità dipendenti (artista, genere e artista) cancellate automaticamente. Tuttavia, non riesco ancora a eliminare un artista o un genere senza prima eliminare tutti i record in cui i loro ID appaiono in ArtistGenre.
Il rapporto sulla cascata non è causato dalla relazione Molti a molti tra Artisti e Generi, ma dal rapporto uno-a-molti Museo - Artisti e Museo - Generi.
L'errore significa che se si rimuovesse un museo si rimuoverebbe automaticamente i suoi artisti e i suoi generi. Durante l'eliminazione di un genere, è necessario regolare tutti gli artisti che si esibiscono in questo genere, ma si eliminano anche gli artisti a causa della rimozione del museo, quindi stiamo rimuovendo in un cerchio.
Puoi vedere che la relazione uno-a-molti con il Museo è il problema, rimuovendo il Museo dal tuo DbContext e creando uno standard molti-a-molti tra Artisti e Generi. Funziona senza attributi o API fluente.
Quindi devi promettere al costruttore del modello che rimuoverai solo i musei che non hanno più Artisti o Generi. Dovrai rimuovere Artisti e generi esposti in un museo prima di poter rimuovere il Museo.
Ho visto anche altre strane cose che non sono necessarie usando il framework di entità. Non sono sicuro che siano necessari a causa di ef-core
.
Ad esempio, i tuoi musei hanno una List
di Artists
, sei sicuro che Artists[17]
sia un'azione significativa? E perché stai creando questi elenchi nel costruttore? Se si preleva un artista dal database, questi elenchi vengono creati dal costruttore e immediatamente sostituiti dai dati recuperati dal database. Terzo: questi elenchi sono realmente elenchi o sono semplicemente interfacce con alcuni dati recuperati dal database? Potrebbe essere che la funzionalità ICollection degli artisti e dei generi del museo sia tutta la funzionalità che utilizzerai, o pensi davvero di utilizzare le funzionalità fornite da un IList?
Se ci si attiene alle convenzioni del codice framework dell'entità , il framework di entità è perfettamente in grado di determinare le chiavi e le relazioni primarie e esterne tra le tabelle semplicemente osservando le proprietà delle classi.
L'unica cosa che il framework di entità non può rilevare automaticamente è che prometti di rimuovere solo Musei senza Generi e senza Artisti.
Ho provato questo tutto usando le seguenti classi:
class Museum
{
public int Id { get; set; }
public string Name { get; set; }
// every Museum has zero or more Artists (one-to-many)
public virtual ICollection<Artist> Artists { get; set; }
// every Museum has zero or more Genres (one-to-many)
public virtual ICollection<Genre> Genres { get; set; }
}
artisti:
class Artist
{
public int Id { get; set; }
// every Artist belongs to one Museum using foreign key:
public int MuseumId { get; set; }
public virtual Museum Museum { get; set; }
// every Artist creates work in zero or more Genres (many-to-many)
public virtual ICollection<Genre> Genres { get; set; }
public string Name { get; set; }
}
generi:
class Genre
{
public int Id { get; set; }
// every Genre belongs to only one Museum using foreign key
public int MuseumId { get; set; }
public virtual Museum Museum { get; set; }
// every Genre is performed by zero or more Artists (many-to-many)
public virtual ICollection<Artist> Artists { get; set; }
public string Name { get; set; }
}
Finalmente il DbContext
class MuseumContext : DbContext
{
public DbSet<Museum> Museums { get; set; }
public DbSet<Artist> Artists { get; set; }
public DbSet<Genre> Genres { get; set; }
}
Poiché mi sono attenuto alle convenzioni code-first, questo era tutto ciò che il framework delle entità aveva bisogno di sapere per configurare le chiavi primarie e quelle estranee, e le relazioni uno-a-molti. Crea persino una tabella di giunzione in più per rappresentare la relazione molti-a-molti tra Artisti e Generi.
L'unica cosa che dobbiamo fare è dire al ModelBuilder di non fare il Cascade On Delete:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var museumEntity = modelBuilder.Entity<Museum>();
museumEntity.HasMany(museum => museum.Genres)
.WithRequired(genre => genre.Museum)
.HasForeignKey(genre => genre.MuseumId)
.WillCascadeOnDelete(false);
museumEntity.HasMany(museum => museum.Artists)
.WithRequired(artist => artist.Museum)
.HasForeignKey(artist => artist.MuseumId)
.WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
Ciò significa: ogni museo ha una proprietà Generi. Ogni elemento del genere ha un museo non nullo, il che significa che ogni genere è associato a un solo museo (né zero, né due). Questo allegato è fatto dalla chiave straniera MuseumId. Se si intende eliminare un museo, il database non deve eliminare tutti i suoi generi
Di conseguenza, Genre.MuseumId potrebbe non essere zero, né cancellare un museo mentre ci sono ancora generi con un museo che indicano questo museo.
Ho provato questo come segue:
Database.SetInitializer<MuseumContext>(new DropCreateDatabaseIfModelChanges<MuseumContext>());
using (var dbContext = new MuseumContext())
{
Genre goldenAge = dbContext.Genres.Add(new Genre() { Name = "Dutch Golden Age Painting" });
Genre renaissance = dbContext.Genres.Add(new Genre() { Name = "Early Renaissance" });
Genre portrets = dbContext.Genres.Add(new Genre() { Name = "Portrets" });
Genre popArt = dbContext.Genres.Add(new Genre() { Name = "Pop Art" });
// The RijksMuseum Amsterdam has a Collection of several genres,
Museum rijksMuseum = dbContext.Museums.Add(new Museum()
{
Name = "RijksMuseum Amsterdam",
Genres = new List<Genre>()
{
goldenAge,
renaissance,
portrets,
},
});
// Rembrandt van Rijn can be seen in the Rijksmuseum:
Artist artist = dbContext.Artists.Add(new Artist()
{
Name = "Rembrandt van Rijn",
Museum = rijksMuseum,
// he painted in several Genres
Genres = new List() {goldenAge, portrets},
});
dbContext.SaveChanges();
}
TODO: guarda se riesci a rimuovere un museo che ha ancora artisti e / o generi
TODO: controlla che, se hai rimosso tutti gli artisti e tutti i generi da un museo, il museo può essere rimosso.
Infine: sei sicuro che un artista sia esposto solo in un museo? C'è un solo museo dove posso vedere un Rembrandt?
Allo stesso modo: è un genere esposto solo in un museo, c'è solo un museo che mostrerà l'impressionismo?
Considera di cambiare queste relazioni in molti a molti. Probabilmente non dovrai più nemmeno usare CascadeOnDelete. Inoltre, se ti senti impertinente sarai in grado di distruggere i musei che hanno ancora degli artisti in loro ;-)