Aller au contenu

Qu'apporte EF Core dans l'accès et la gestion des données ?

Introduction

Entity Framework Core est un ORM (Object-Relational Mapping, ou mapping objet-relationnel), une brique logicielle qui permet de gérer les données contenues dans une base de données relationnelle, au travers d’un modèle orienté objet. Il établit une correspondance (mapping) entre les éléments de la base de données (relational) et ceux de l’application (object), notamment entre :

  • Les tables et les classes

  • Les colonnes et les propriétés

  • Les relations entre les tables et les propriétés de navigation

Entity Framework Core fournit également différents services, tels que le chargement des données, le suivi des modifications apportées aux objets et la persistance de ces modifications dans la base de données.

Pour ce faire, son rôle consiste notamment à convertir automatiquement :

  • Les lignes des tables en objets lors de la lecture des données.

  • Les objets en lignes de tables lors des insertions et des mises à jour.

Il prend aussi en charge la suppression des lignes pour les objets marqués supprimés.

Ainsi, grâce à Entity Framework Core, les développeurs manipulent principalement des objets du modèle de l’application plutôt que des connexions, des commandes SQL, des lecteurs de données (reader) ou des jeux de données (dataset). Il se charge de traduire les opérations réalisées sur ces objets en requêtes SQL.

Voici un schéma qui récapitule les fonctions proposées par Entity Framework Core :

Entity Framework Core - Vue d'ensemble

Pour illustrer l’intérêt d’Entity Framework Core, nous utiliserons une table nommée Customer, dont le script de création pour SQL Server est le suivant :

CREATE TABLE dbo.Customer
(
    Id          bigint IDENTITY(1,1) NOT NULL,
    FirstName   nvarchar(100) NOT NULL,
    LastName    nvarchar(100) NOT NULL,
    Email       nvarchar(255) NOT NULL,
    BirthDate   date NULL,
    IsActive    bit NOT NULL,
    CreatedDate datetime2(7) NOT NULL,

    CONSTRAINT PK_Customer PRIMARY KEY CLUSTERED (Id)
);

CREATE UNIQUE INDEX IX_Customer_Email
ON dbo.Customer (Email);

L’entité correspondante à cette table peut être représentée par la classe suivante :

public class Customer
{
    public long Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string Email { get; set; }

    public DateOnly BirthDate { get; set; }

    public bool IsActive { get; set; }

    public DateTime CreatedDate { get; set; }

    public Customer(long id, string firstName, string lastName, string email, DateOnly birthDate, bool isActive, DateTime createdDate)
    {
        Id = id;
        FirstName = firstName;
        LastName = lastName;
        Email = email;
        BirthDate = birthDate;
        IsActive = isActive;
        CreatedDate = createdDate;
    }
}

Sans Entity Framework Core

Pour comprendre les plus-values d'Entity Framework Core, il peut être nécessaire de visualiser le code que nous pourrions écrire. Et dans ce cas, il revient aux développeurs d’utiliser directement les classes d’ADO.NET pour accéder aux données de la table Customer.

Voici un exemple permettant de rechercher un client à partir de son identifiant :

const string sql = """
    SELECT Id, FirstName, LastName
    FROM dbo.Customer
    WHERE Id = @Id
    """;

using var command = new SqlCommand(sql, connection);

command.Parameters.Add(new SqlParameter("@Id", SqlDbType.BigInt, id));

using var reader = await command.ExecuteReaderAsync();

Customer? customer = null;

if (await reader.ReadAsync())
{
    customer = new Customer(
        reader.GetInt64("Id"),
        reader.GetString("FirstName"),
        reader.GetString("LastName"));
}

Dans cet exemple, le développeur doit prendre en charge :

  • L’écriture de la requête SQL

  • La création et le paramétrage de la commande

  • L'exécution de la requête

  • La lecture des résultats

  • La conversion des données en objet Customer

Cette approche offre un contrôle précis sur l’accès aux données. Elle augmente toutefois la quantité de code technique à écrire, les risques d’erreur et le coût de maintenance. Le mapping entre les colonnes et les propriétés est notamment réalisé manuellement. Toute évolution du modèle ou de la structure de la base de données, peut donc nécessiter la modification de plusieurs portions de code.

Avec Entity Framework Core

Le contexte de données

Avec Entity Framework Core, la base de données est représentée au travers d’un contexte de données, implémentée par une classe dérivant de la classe Microsoft.EntityFrameworkCore.DbContext.

Cette classe expose les différents jeux d’entités manipulés par l’application au moyen de propriétés de type DbSet<T>.

using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Customer> Customers => Set<Customer>();
}

Le mapping

La configuration du mapping entre l'entité et la classe peut être implémentée de manière :

  • Déclarative, via l'utilisation d'attributes sur les classes et les propriétés

  • Impérative, via l'utilisation de l'API Fluent

Voici un exemple en utilisant l'implémentation déclarative :

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

[Table("Customer")]
public class Customer
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Column("Id")]
    public long Id { get; set; }

    [Required]
    [MaxLength(100)]
    [Column("FirstName")]
    public string FirstName { get; set; }

    [Required]
    [MaxLength(100)]
    [Column("LastName")]
    public string LastName { get; set; }

    [Required]
    [MaxLength(500)]
    [Column("Email")]
    public string Email { get; set; }

    [Column("BirthDate")]
    public DateOnly BirthDate { get; set; }

    [Required]
    [Column("IsActive")]
    public bool IsActive { get; set; }

    [Required]
    [Column("CreatedDate")]
    public DateTime CreatedDate { get; set; }

    public Customer(
        long id,
        string firstName,
        string lastName,
        string email,
        DateOnly birthDate,
        bool isActive,
        DateTime createdDate)
    {
        Id = id;
        FirstName = firstName;
        LastName = lastName;
        Email = email;
        BirthDate = birthDate;
        IsActive = isActive;
        CreatedDate = createdDate;
    }
}

La configuration du mapping entre les tables et les entités sera détaillée dans un prochain article.

Lecture de données

La recherche d’un client peut alors être réalisée avec cette requête LINQ :

using Microsoft.EntityFrameworkCore;

var options = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseSqlServer(
        "Server=.\\SqlExpress;Database=DbDemo;Trusted_Connection=True")
    .Options;

using var context = new ApplicationDbContext(options);

var customer = await context.Customers
    .FirstOrDefaultAsync(customer => customer.Id == 1);

Entity Framework Core se charge automatiquement :

  • De générer une requête SQL à partir de la requête Linq To Entities

  • De se connecter / déconnecter de la base de données

  • De créer et d’exécuter la commande

  • De lire les données retournées, de créer et d'initialiser l’objet de type Customer

  • Puis, de suivre les éventuelles modifications des propriétés et la suppression des objets

Le code concernant l’accès aux données est ainsi plus concis et davantage centré sur le modèle métier de l’application que sur le stockage des données.

Le suivi des modifications

Entity Framework Core ne se limite pas au chargement des données. Par défaut, il suit également les modifications apportées aux entités chargées (via un état, appelé Entity State).

var customer = await context.Customers
    .FirstOrDefaultAsync(customer => customer.Id == id);

if (customer is not null)
{
    customer.LastName = "RAVAILLE";

    await context.SaveChangesAsync();
}

Lors de l’appel à la méthode SaveChangesAsync(), Entity Framework Core détecte que la propriété LastName a été modifiée et génère automatiquement la requête SQL de type UPDATE permettant de mettre à jour la ligne correspondante dans la table Customer.

Sans Entity Framework Core, cette requête devrait être écrite et exécutée manuellement. Cependant, il ne supprime toutefois pas toute complexité. Une requête Linq To Entities ne respectant pas les bonnes pratiques (un article sera consacré à l'écriture de requête pour optimiser les performances) peut générer une requête SQL inefficace, charger une quantité excessive de données ou provoquer un grand nombre d’accès à la base de données.

Est-il nécessaire de connaître le langage SQL ?

Une idée reçue consiste à penser qu’Entity Framework Core permet de se passer complètement du langage SQL. Pour concevoir des applications performantes, il reste indispensable de comprendre les principes des bases de données relationnelles, de savoir lire les requêtes produites et, lorsque cela est nécessaire, d’analyser leurs plans d’exécution.

La connaissance de SQL permet notamment de vérifier :

  • La pertinence des jointures générées

  • L'utilisation appropriée des index

  • Le volume de données chargées par les requêtes SQL

  • Les opérations réalisées par le serveur de base de données lors de l'exécution des requêtes