Base de données

Quand une application a besoin de stocker puis de retrouver des informations sur la durée elle utilise en général une base de donnée.

Le framework ASP.Net Core nous permet facilement de se connecter a une BDD puis d'y faire des requêtes.

Les base de données supporté

Il existe plusieurs base supporté nottament:

  • SQL Server
  • Postgres
  • MariaDB
  • Mysql
  • Sqlite
  • MongoDB

On peux très facilement se connecter a d'autre base en ajoutant une dépendance via le système Nuget.

Sqlite

Cette base est particulièrement intéressante car elle n'a pas besoin de serveur pour fonctionner. En effet le stockage des information se fait dans un fichier sur le disque.

Cela est très pratique pour effectuer des tests ou pour des petites applications.

BDD en concept objet

Le stockage et la récupération des informations utilise les classes défini dans le projet. Il faudra donc définir une classe qui représente l'objet que l'on souhaite stocker.

Voici un exemple pour ajouter une base Sqlite a un projet.

Pour cet exemple nous gérerons des Confiture

public class Confiture
{
    public int Id { get; set; }
    public string Fruit { get; set; }
    public int Annee { get; set; }

    public Confiture(int id, string fruit, int annee)
    {
        Id = id;
        Fruit = fruit;
        Annee = annee;
    }
}

Maintenant que l'on a défini la structure de notre Confiture passons a la définition de notre BDD.

Instalation des dépendances

Pour pouvoir sauvegarder nos données, nous allons utiliser l’entity framework.

C’est un framework qui s’occupe d’abstraire les requêtes vers la base pour nous.

Pour cela, il faut l’ajouter à notre projet. Nous en profiterons pour ajouter dotnet ef qui permettra d’initialiser notre base de donnée

pour se faire, executer ces commandes dans le répértoire du projet depuis un terminal.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.*
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 8.*
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.*
dotnet tool install --global dotnet-ef --version 8.*

Définir notre connection a une BDD

Pour défini une BDD, il faut créer une classe qui hérite de DbContext. Le nom de cette nouvelle classe se termine en général par Context. La classe doit posséder un constructeur.

Puis pour chaque type d'élement à stocker on va créer une variable de type DbSet<Type>

Voila donc a quoi ressemble une classe basique

using Microsoft.EntityFrameworkCore;

namespace ConfitureApi.Models;

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

    public DbSet<Confiture> Confitures { get; set; } = null!;
}

Configurer Sqlite

Notre base n'est actuellement pas configuré pour utiliser Sqlite.

On va ajouter une méthode OnConfiguring qui sera appelé pour configurer notre connection à la BDD. Dans cette méthode on va spécifier que l'on désire utiliser Sqlite et on donnera le lien vers le fichier de la base.

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

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // Connexion a la base sqlite
        options.UseSqlite("Data Source=Confiture.db");
    }

    public DbSet<Confiture> Confitures { get; set; } = null!;
}

Initialiser la BDD au lancement

Pour que notre programme se connecte a notre BDD il faut le lui demander. Cela se passe dans la phase de configuration dans Program.cs

builder.Services.AddDbContext<ConfitureContext>();
Attention, il est important de ne pas oublier d'ajouter le package `Microsoft.EntityFrameworkCore.Sqlite` pour que le code fonctionne.

Les Migrations

On va créer un fichier de migration qui nous permettra de créer la structure de notre base.

Une migration est un fichier qui permet de faire passer une base d'un état A à un état B mais également de revenir à l'état A en efféctuant les opérations inverse.

Dans notre cas, ce premier fichier de migration va servir a créer notre table Confitures avec ses champs.

La encore, c'est une commande qui va nous aider

dotnet ef migrations add InitialisationDeLaDB

Un nouveau dossier Migrations a du apparaitre. A l'intérieur 3 fichiers dont un qui devrait ressembler a ca :

using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace ConfitureApi.Migrations
{
    /// <inheritdoc />
    public partial class Init : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            // La liste des opérations à executer pour créer notre table Confitures
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            // Les opération inverse, donc ici la suppression de la table Confitures
        }
    }
}

Création de la BDD

Notre applicatif connait désormais notre BDD et s'y connecte au démarrage.

Mais elle n'existe pas encore cette BDD !

dotnet ef database update

Cette commande va commencer par créer notre BDD Sqlite si elle n'existe pas.

Puis elle va y appliquer les migrations une par une dans l'ordre.

Dans notre cas, une seul migration sera appliqué, pour créer notre table Confitures.

Utiliser la BDD dans un controlleur

Attention, il est important d'ajouter le using suivant en haut du Controller pour que le code qui suit fonctionne
using Microsoft.EntityFrameworkCore;

Pour accéder à notre BDD dans un controlleur il faut injecter notre contexte dans le constructeur. On créer une variable privée pour stocker le contexte et on l'initialise dans le constructeur.

using ConfitureApi.Models;

namespace ConfitureApi.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ConfitureController : ControllerBase
{

    private readonly ConfitureContext _context;
    public ConfitureController(ConfitureContext ctx)
    {
        _context = ctx;
    }
}

Récupérer des données

Pour récupérer des données de notre BDD on va utiliser la méthode Find de notre contexte.

[HttpGet("{id}")]
public async Task<ActionResult<Confiture>> GetConfiture(int id)
{
    // on récupère la confiture correspondant a l'id
    var confiture = await _context.Confitures.FindAsync(id);

    if (confiture == null)
    {
        return NotFound();
    }
    // on retourne la confiture
    return Ok(confiture);
}

Ajouter des données

Pour ajouter des données dans notre BDD on va utiliser la méthode Add de notre contexte.

Puis on va appeler la méthode SaveChanges pour enregistrer les modifications.

On peux ensuite retourner un code 201 pour indiquer que la création a bien eu lieu.

class ConfitureCreation
{
    public string Fruit { get; set; }
    public int Annee { get; set; }
}

[HttpPost]
public async Task<ActionResult<Confiture>> PostConfiture(ConfitureCreation confitureCreation)
{
    // on créer une nouvelle confiture avec les informations reçu
    Confiture confiture = new Confiture {
        Fruit = confitureCreation.Fruit, 
        Annee = confitureCreation.Annee
    };
    // on l'ajoute a notre contexte (BDD)
    _context.Confitures.Add(confiture);
    // on enregistre les modifications dans la BDD ce qui remplira le champ Id de notre objet
    await _context.SaveChangesAsync();
    // on retourne un code 201 pour indiquer que la création a bien eu lieu
    return CreatedAtAction(nameof(GetConfiture), new { id = confiture.Id }, confiture);
}

Mettre a jour des données

Pour mettre a jour des données dans notre BDD on va utiliser la méthode Update de notre contexte.

Puis on va appeler la méthode SaveChanges pour enregistrer les modifications.

On peux ensuite retourner un code 204 pour indiquer que la modification a bien eu lieu.

Le code est un peu plus complexe car il faut vérifier que l'objet que l'on souhaite modifier n'a pas été modifié entre temps.

On utilise donc un try catch pour gérer l'erreur DbUpdateConcurrencyException.

[HttpPut("{id}")]
public async Task<IActionResult<Confiture>> PutConfiture(Confiture confitureUpdate)
{
    // on récupère la confiture que l'on souhaite modifier
    Confiture confiture = await _context.Confitures.FindAsync(confitureUpdate.Id);
    if (confiture == null)
    {
        return NotFound();
    }

    // on met a jour les informations de la confiture
    confiture.Fruit = confitureUpdate.Fruit;
    confiture.Annee = confitureUpdate.Annee;
    // on indique a notre contexte que l'objet a été modifié
    _context.Entry(confiture).State = EntityState.Modified;

    try
    {
        // on enregistre les modifications
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        // si une erreur de concurrence survient on retourne un code 500
        return StatusCode(500, "Erreur de concurrence");
    }
    // on retourne un code 200 pour indiquer que la modification a bien eu lieu
    return Ok(confiture);
}

Supprimer des données

Pour supprimer des données dans notre BDD on va utiliser la méthode Remove de notre contexte.

Puis on va appeler la méthode SaveChanges pour enregistrer les modifications.

On peux ensuite retourner un code 204 pour indiquer que la suppression a bien eu lieu.

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteConfiture(int id)
{
    // on récupère la confiture que l'on souhaite supprimer
    Confiture confiture = await _context.Confitures.FindAsync(id);
    if (confiture == null)
    {
        return NotFound();
    }
    // on indique a notre contexte que l'objet a été supprimé
    _context.Confitures.Remove(confiture);
    // on enregistre les modifications
    await _context.SaveChangesAsync();
    // on retourne un code 204 pour indiquer que la suppression a bien eu lieu
    return NoContent();
}

Conclusion

Les bases de données sont un élément essentiel de toute application.

ASP.Net Core nous permet de facilement nous connecter a une base et d'y effectuer des opérations sans intégrer de logique sépcifique à la base choisi.