47 votes

API fluide, many-to-many dans Entity Framework Core

J'ai cherché sur stackoverflow une solution appropriée pour générer un fichier de type plusieurs à plusieurs relation, en utilisant EF Core, Code first et Fluent API.

Un scénario simple serait :

public class Person
{
    public Person() {
        Clubs = new HashSet<Club>();
    }
    public int PersonId { get; set; }
    public virtual ICollection<Club> Clubs { get; set; }
}

public class Club
{
    public Club() {
        Persons = new HashSet<Person>();
    }
    public int ClubId { get; set; }
    public virtual ICollection<Person> Persons { get; set; }
}

Corrigez-moi si je me trompe, mais je n'ai pas trouvé de question contenant une explication détaillée sur la façon de procéder à l'aide des outils décrits. Quelqu'un peut-il m'expliquer comment faire ?

60voto

Kirk Larkin Points 29405

EF Core 5.0 RC1+

Depuis EF Core 5.0 RC1, il est possible de le faire sans table de jointure explicite. EF Core est capable de configurer un mapping pour la relation many-to-many présentée dans votre question sans que vous ayez à créer une table de jointure. PersonClub type.

Voir Nouveautés dans EF Core 5.0, RC1, Many-to-many dans les documents officiels pour plus d'informations.

Versions précédentes

Ceci n'est pas encore possible dans EF Core sans utiliser une classe explicite pour la jointure. Voir aquí pour un exemple de la façon de procéder.

Il y a un ouvert numéro sur Github demandant la possibilité de faire cela sans avoir besoin d'une classe explicite, mais il n'a pas encore été complété.

En utilisant votre scénario, l'exemple que j'ai lié recommanderait les classes d'entités suivantes :

public class Person
{
    public int PersonId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class Club
{
    public int ClubId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class PersonClub
{
    public int PersonId { get; set; }
    public Person Person { get; set; }
    public int ClubId { get; set; }
    public Club Club { get; set; }
}

Les éléments suivants OnModelCreating serait alors utilisé pour la configuration :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<PersonClub>()
        .HasKey(pc => new { pc.PersonId, pc.ClubId });

    modelBuilder.Entity<PersonClub>()
        .HasOne(pc => pc.Person)
        .WithMany(p => p.PersonClubs)
        .HasForeignKey(pc => pc.PersonId);

    modelBuilder.Entity<PersonClub>()
        .HasOne(pc => pc.Club)
        .WithMany(c => c.PersonClubs)
        .HasForeignKey(pc => pc.ClubId);
}

N'hésitez pas à consulter la question ouverte que j'ai liée et à exprimer votre frustration si vous en ressentez le besoin.

EDIT : Le problème ouvert suggère d'utiliser un simple Select pour naviguer dans cette hiérarchie quelque peu encombrante. Afin de passer d'un PersonId à une collection de Club vous pouvez utiliser SelectMany . par exemple :

var clubs = dbContext.People
    .Where(p => p.PersonId == id)
    .SelectMany(p => p.PersonClubs);
    .Select(pc => pc.Club);

Je ne peux pas garantir qu'il s'agit vraiment d'une "meilleure pratique", mais cela devrait faire l'affaire et je pense qu'il est juste de dire que ce n'est pas trop laid.

0 votes

La migration vers 5.0 a été facile et a fonctionné parfaitement <3

0 votes

Pouvez-vous fournir les méthodes de ClubsController à titre d'exemple ? J'ai besoin de quelque chose comme [EnableQuery] public IActionResult Get() { return Ok(_dbContext.Clubs .Include(x => x.PersonClubs) .ThenInclude(x => x.Persons)) ; } mais cela ne fonctionne pas.

25voto

paul van bladel Points 673

La "configuration" correcte pour cela est :

public class Person
{
    public int PersonId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class Club
{
    public int ClubId { get; set; }
    public virtual ICollection<PersonClub> PersonClubs { get; set; }
}

public class PersonClub
{
    public int PersonId { get; set; }
    public Person Person { get; set; }
    public int ClubId { get; set; }
    public Club Club { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<PersonClub>()
        .HasKey(pc => new { pc.PersonId, pc.ClubId });
}

Donc, ce bloc pour configurer la "glue-table" est no nécessaire comme dans l'exemple de @Kirk :

modelBuilder.Entity<PersonClub>()
    .HasOne(pc => pc.Person)
    .WithMany(p => p.PersonClubs)
    .HasForeignKey(pc => pc.PersonId);

modelBuilder.Entity<PersonClub>()
    .HasOne(pc => pc.Club)
    .WithMany(c => c.PersonClubs)
    .HasForeignKey(pc => pc.ClubId);

4 votes

Si vous utilisez l'attribut [ForeignKey] dans l'entité de lien, vous n'avez pas besoin de code. Malgré tout, il est pathétique que cela ne fonctionne pas comme la version précédente. En 4 ans de développement pour Core, ils n'ont pas été capables de faire correspondre la version précédente.

7 votes

Non nécessaire ne veut pas dire incorrect . Être explicite dans la configuration ne fait pas de mal, et permet même souvent d'éviter les surprises causées par les conventions implicites d'EF Core. Il y a une demande pour fournir un moyen de se débarrasser de toutes les conventions et d'exiger une configuration explicite pour tout.

1 votes

Qu'en est-il des mappings fluides pour Person y Club ?

3voto

HaraldDutch Points 225

Donc chaque Person a zéro ou plus Clubs et chaque Club a zéro ou plus Persons . Comme vous l'avez dit correctement, il s'agit d'une relation many-to-many appropriée.

Vous savez probablement qu'une base de données relationnelle a besoin d'une table supplémentaire pour mettre en œuvre cette relation entre plusieurs personnes. L'avantage de l'entity framework est qu'il reconnaît cette relation et crée cette table supplémentaire pour vous.

À première vue, le fait que cette table supplémentaire ne soit pas une dbSet en su DbContext : " Comment effectuer une jointure avec cette table supplémentaire si je n'ai pas de DbSet pour cela ?".

Heureusement, vous n'avez pas besoin de mentionner cette table supplémentaire dans vos requêtes.

Si vous avez besoin d'une requête du type "Donnez-moi tous les 'Clubs' qui ... de chaque 'Personne' qui ...", ne pensez pas en termes de jointures. Utilisez plutôt les ICollections !

Obtenez toutes les personnes "John Doe" avec tous les Country clubs qu'ils fréquentent :

var result = myDbContext.Persons
    .Where(person => person.Name == "John Doe")
    .Select(person => new
    {
        PersonId = person.Id,
        PersonName = person.Name,
        AttendedCountryClubs = person.Clubs
            .Where(club => club.Type = ClubType.CountryClub),
    };

Entity Framework reconnaîtra qu'une jointure avec la table supplémentaire many-to-many est nécessaire, et effectuera cette jointure, sans que vous mentionniez cette table supplémentaire.

Dans l'autre sens : Obtenez tous les country clubs avec leurs personnes "John Doe" :

var result = myDbContext.Clubs
    .Where(club => club.Type = ClubType.CountryClub)
    .Select(club => new
    {
         ClubId = club.Id,
         ClubName = club.Name,
         AnonymousMembers = club.Persons
             .Where(person => person.Name == "John Doe"),
    }

J'ai fait l'expérience qu'une fois que j'ai commencé à penser aux collections résultantes que je veux au lieu des jointures dont j'avais besoin pour obtenir ces collections, je me suis rendu compte que j'utilisais à peine les jointures. C'est le cas pour les relations un-à-plusieurs ainsi que pour les relations plusieurs-à-plusieurs. Entity Framework utilisera en interne les jointures appropriées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X