1. Concepts de base de l'entité et de l'association

Dans le cadre de l'ent, une entité fait référence à l'unité de données de base gérée dans la base de données, qui correspond généralement à une table dans la base de données. Les champs de l'entité correspondent aux colonnes de la table, tandis que les associations (arêtes) entre les entités sont utilisées pour décrire les relations et les dépendances entre les entités. Les associations d'entités constituent la base pour la construction de modèles de données complexes, permettant la représentation de relations hiérarchiques telles que les relations parent-enfant et les relations de propriété.

Le cadre ent offre un ensemble riche d'API, permettant aux développeurs de définir et de gérer ces associations dans le schéma de l'entité. Grâce à ces associations, nous pouvons facilement exprimer et opérer sur la logique métier complexe entre les données.

2. Types d'associations d'entités dans ent

2.1 Association Un à Un (O2O)

Une association un à un fait référence à une correspondance un à un entre deux entités. Par exemple, dans le cas des utilisateurs et des comptes bancaires, chaque utilisateur ne peut avoir qu'un seul compte bancaire, et chaque compte bancaire appartient également à un seul utilisateur. Le cadre ent utilise les méthodes edge.To et edge.From pour définir de telles associations.

Tout d'abord, nous pouvons définir une association un à un pointant vers Carte dans le schéma Utilisateur :

// Arêtes de l'utilisateur.
func (Utilisateur) Arêtes() []ent.Arc {
    return []ent.Arc{
        edge.To("carte", Carte.Type). // Pointe vers l'entité Carte, en définissant le nom de l'association comme "carte"
            Unique(),               // La méthode Unique garantit qu'il s'agit d'une association un à un
    }
}

Ensuite, nous définissons l'association inverse vers Utilisateur dans le schéma Carte :

// Arêtes de la Carte.
func (Carte) Arêtes() []ent.Arc {
    return []ent.Arc{
        edge.From("propriétaire", Utilisateur.Type). // Pointe de nouveau vers l'utilisateur depuis la Carte, en définissant le nom de l'association comme "propriétaire"
            Réf.("Carte").              // La méthode Réf. spécifie le nom de l'association inverse correspondant
            Unique(),                 // Marqué comme unique pour garantir qu'une carte correspond à un seul propriétaire
    }
}

2.2 Association Un à Plusieurs (O2M)

Une association un à plusieurs indique qu'une entité peut être associée à plusieurs autres entités, mais que ces entités peuvent uniquement pointer vers une entité unique. Par exemple, un utilisateur peut avoir plusieurs animaux de compagnie, mais chaque animal de compagnie n'a qu'un seul propriétaire.

Dans ent, nous utilisons toujours edge.To et edge.From pour définir ce type d'association. L'exemple ci-dessous définit une association un à plusieurs entre les utilisateurs et les animaux de compagnie :

// Arêtes de l'utilisateur.
func (Utilisateur) Arêtes() []ent.Arc {
    return []ent.Arc{
        edge.To("animauxDeCompagnie", AnimalDeCompagnie.Type), // Association un à plusieurs de l'entité Utilisateur à l'entité Animal de compagnie
    }
}

Dans l'entité AnimalDeCompagnie, nous définissons une association de plusieurs à un retour vers Utilisateur :

// Arêtes de l'Animal de Compagnie.
func (AnimalDeCompagnie) Arêtes() []ent.Arc {
    return []ent.Arc{
        edge.From("propriétaire", Utilisateur.Type). // Association de plusieurs à un de AnimalDeCompagnie à Utilisateur
            Réf.("animauxDeCompagnie").              // Spécifie le nom de l'association inverse de animal de compagnie à propriétaire
            Unique(),                 // Assure qu'un propriétaire peut avoir plusieurs animaux de compagnie
    }
}

2.3 Association Plusieurs à Plusieurs (M2M)

Une association plusieurs à plusieurs permet à deux types d'entités d'avoir plusieurs instances l'une de l'autre. Par exemple, un étudiant peut s'inscrire à plusieurs cours, et un cours peut également avoir plusieurs étudiants inscrits. ent fournit une API pour établir des associations plusieurs à plusieurs :

Dans l'entité Étudiant, nous utilisons edge.To pour établir une association plusieurs à plusieurs avec Cours :

// Arêtes de l'Étudiant.
func (Étudiant) Arêtes() []ent.Arc {
    return []ent.Arc{
        edge.To("cours", Cours.Type), // Définit une association plusieurs à plusieurs de l'Étudiant à Cours
    }
}

De même, dans l'entité Cours, nous établissons une association inverse vers Étudiant pour la relation plusieurs à plusieurs :

// Arêtes du Cours.
func (Cours) Arêtes() []ent.Arc {
    return []ent.Arc{
        edge.From("étudiants", Étudiant.Type). // Définit une association plusieurs à plusieurs de Cours à Étudiant
            Réf.("cours"),                  // Spécifie le nom de l'association inverse de Cours à Étudiant
    }
}

Ces types d'associations sont la pierre angulaire de la construction de modèles de données d'application complexes, et comprendre comment les définir et les utiliser dans ent est crucial pour étendre les modèles de données et la logique métier.

3. Opérations de base pour les associations entité

Cette section va démontrer comment effectuer les opérations de base en utilisant ent avec les relations définies, y compris la création, la requête et le parcours des entités associées.

3.1 Création d'entités associées

Lors de la création d'entités, vous pouvez simultanément définir les relations entre les entités. Pour les relations un-à-plusieurs (O2M) et plusieurs-à-plusieurs (M2M), vous pouvez utiliser la méthode Add{Edge} pour ajouter des entités associées.

Par exemple, si nous avons une entité utilisateur et une entité animal de compagnie avec une association spécifique, où un utilisateur peut avoir plusieurs animaux de compagnie, voici un exemple de création d'un nouvel utilisateur et d'ajout d'animaux de compagnie pour celui-ci :

// Créer un utilisateur et ajouter des animaux de compagnie
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Créer une instance d'animal de compagnie
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Créer une instance d'utilisateur et l'associer à l'animal de compagnie
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Utiliser la méthode AddPets pour associer l'animal de compagnie
        SaveX(ctx)

    return user, nil
}

Dans cet exemple, nous créons d'abord une instance d'animal de compagnie nommée Fido, puis nous créons un utilisateur nommé Alice et associons l'instance de l'animal de compagnie à l'utilisateur en utilisant la méthode AddPets.

3.2 Requête des entités associées

La requête des entités associées est une opération courante dans ent. Par exemple, vous pouvez utiliser la méthode Query{Edge} pour récupérer d'autres entités associées à une entité spécifique.

Poursuivant notre exemple d'utilisateurs et d'animaux de compagnie, voici comment interroger tous les animaux de compagnie possédés par un utilisateur :

// Interroger tous les animaux de compagnie d'un utilisateur
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Obtenir l'instance utilisateur basée sur l'ID utilisateur
        QueryPets().      // Interroger les entités animal de compagnie associées à l'utilisateur
        All(ctx)          // Retourner toutes les entités animal de compagnie interrogées
    if err != nil {
        return nil, err
    }

    return pets, nil
}

Dans le code ci-dessus, nous obtenons d'abord l'instance utilisateur basée sur l'ID utilisateur, puis appelons la méthode QueryPets pour récupérer toutes les entités animal de compagnie associées à cet utilisateur.

Remarque : L'outil de génération de code de ent génère automatiquement l'API pour les requêtes d'association en fonction des relations d'entité définies. Il est recommandé de passer en revue le code généré.

4. Chargement anticipé

4.1 Principes du préchargement

Le préchargement est une technique utilisée dans la requête de bases de données pour récupérer et charger les entités associées à l'avance. Cette approche est couramment utilisée pour obtenir des données liées à plusieurs entités en une seule opération, afin d'éviter de multiples opérations de requête de base de données dans le traitement ultérieur, améliorant ainsi considérablement les performances de l'application.

Dans le framework ent, le préchargement est principalement utilisé pour gérer les relations entre les entités, telles que un-à-plusieurs et plusieurs-à-plusieurs. Lors de la récupération d'une entité de la base de données, ses entités associées ne sont pas chargées automatiquement. Au lieu de cela, elles sont explicitement chargées au besoin via le préchargement. Cela est crucial pour résoudre le problème de requête N+1 (c'est-à-dire, effectuer des requêtes séparées pour chaque entité parent).

Dans le framework ent, le préchargement est réalisé en utilisant la méthode With dans le générateur de requête. Cette méthode génère des fonctions With... correspondantes pour chaque bord, tels que WithGroups et WithPets. Ces méthodes sont automatiquement générées par le framework ent, et les programmeurs peuvent les utiliser pour demander le préchargement d'associations spécifiques.

Le principe de fonctionnement des entités préchargées est que lors de la requête de l'entité principale, ent exécute des requêtes supplémentaires pour récupérer toutes les entités associées. Ensuite, ces entités sont ajoutées dans le champ Edges de l'objet retourné. Cela signifie que ent peut exécuter plusieurs requêtes de base de données, au moins une pour chaque bord associé qui doit être préchargé. Bien que cette méthode puisse être moins efficace qu'une seule requête complexe JOIN dans certains scénarios, elle offre une plus grande flexibilité et devrait bénéficier d'optimisations de performances dans les versions futures de ent.

4.2 Implémentation du préchargement

Nous allons maintenant démontrer comment effectuer les opérations de préchargement dans le framework ent à travers un exemple de code, en utilisant les modèles d'utilisateurs et d'animaux de compagnie décrits dans l'aperçu.

Préchargement d'une Association Unique

Supposons que nous voulions récupérer tous les utilisateurs de la base de données et précharger les données des animaux de compagnie. Nous pouvons y parvenir en écrivant le code suivant :

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Gérer l'erreur
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("L'utilisateur (%v) possède l'animal de compagnie (%v)\n", u.ID, p.ID)
    }
}

Dans cet exemple, nous utilisons la méthode WithPets pour demander à ent de précharger les entités d'animaux de compagnie associées aux utilisateurs. Les données d'animaux de compagnie préchargées sont intégrées dans le champ Edges.Pets, à partir duquel nous pouvons accéder à ces données associées.

Préchargement de Multiples Associations

ent nous permet de précharger plusieurs associations à la fois, et même de spécifier le préchargement d'associations imbriquées, de filtrer, de trier ou de limiter le nombre de résultats préchargés. Voici un exemple de préchargement des animaux de compagnie des administrateurs et des équipes auxquelles ils appartiennent, tout en préchargeant également les utilisateurs associés aux équipes :

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Limiter aux 5 premières équipes
        q.Order(ent.Asc(group.FieldName)) // Trier par ordre croissant par nom d'équipe
        q.WithUsers()       // Précharger les utilisateurs de l'équipe
    }).
    All(ctx)
if err != nil {
    // Gérer les erreurs
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("L'administrateur (%v) possède l'animal de compagnie (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("L'administrateur (%v) appartient à l'équipe (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("L'équipe (%v) a pour membre (%v)\n", g.ID, u.ID)
        }
    }
}

À travers cet exemple, vous pouvez voir à quel point ent est puissant et flexible. Avec seulement quelques appels de méthode simples, il peut précharger des données associées riches et les organiser de manière structurée. Cela offre une grande commodité pour le développement d'applications basées sur les données.