Licence CC BY

Manipuler ses entités avec EF

Les auteurs de ce contenu recherchent un correcteur. N’hésitez pas à les contacter par MP pour proposer votre aide !

C’est maintenant que tout commence. Nous allons apprendre à manipuler nos entités et à les persister, c’est-à-dire les enregistrer dans la base de données.

Comme la base de données est un système puissant, il nous permet de lui laisser faire un certains nombres de traitements que nous devrions théoriquement faire dans le contrôleur.

Cette partie vous présentera donc comment obtenir les données sauvegardées dans la base de données, les filtrer, comment les sauvegarder.

Nous terminerons en présentant le patron de conception "Repository" aussi appelé "Bridge", afin de rendre notre code plus propre et maintenable.

Obtenir, filtrer, ordonner vos entités

Allons donc dans notre contrôleur ArticleController.cs.

La première étape sera d’ajouter une instance de notre contexte de données en tant qu’attribut du contrôleur.

public class ArticleController{

    private ApplicationDbContext bdd = ApplicationDbContext.Create();//le lien vers la base de données
    /* reste du code*/
}
Ajouter un lien vers la base de données

Une fois ce lien fait, nous allons nous rendre dans la méthode List et nous allons remplacer le vieux code par une requête à notre base de données.

 try
{
   List<Article> liste = _repository.GetAllListArticle().ToList();
   return View(liste);
}
catch
{
    return View(new List<Article>());
}
L’ancien code

Entity Framework use Linq To SQL pour fonctionner. Si vous avez déjà l’habitude de cette technologie, vous ne devriez pas être perdu.

Sinon, voici le topo :

Linq, pour Language Integrated Query, est un ensemble de bibliothèques d’extensions codées pour vous simplifier la vie lorsque vous devez manipuler des collections de données.

Ces collections de données peuvent être dans des tableaux, des listes, du XML, du JSON, ou bien une base de données !

Quoi qu’il arrive, les méthodes utilisées seront toujours les mêmes.

Obtenir la liste complète

Il n’y a pas de code plus simple : bdd.Articles.ToList();. Et encore, le ToList() n’est nécessaire que si vous désirez que votre vue manipule une List<Article> au lieu d’un IEnumerable<Article>. La différence entre les deux : la liste est totalement chargée en mémoire alors que le IEnumerable ne fera en sorte de charger qu’un objet à la fois.

Certains cas sont plus pratiques avec une liste, donc pour rester le plus simple possible, gardons cette idée !

Obtenir une liste partielle (retour sur la pagination)

Souvenez-vous de l’ancien code :

public ActionResult List(int page = 0)
        {
            try
            {
                List<Article> liste = _repository.GetListArticle(page * ARTICLEPERPAGE, ARTICLEPERPAGE).ToList();
                ViewBag.Page = page;
                return View(liste);
            }
            catch
            {
                return View(new List<Article>());
            }
        }
La pagination dans l’ancienne version

Notre idée restera la même avec Linq. Par contre Linq ne donne pas une méthode qui fait tout à la fois.

Vous n’aurez droit qu’à deux méthodes :

  • Take : permet de définir le nombre d’objet à prendre ;
  • Skip : permet de définir le saut à faire.

Néanmoins, avant toute chose, il faudra que vous indiquiez à EntityFramework comment ordonner les entités.

Pour s’assurer que notre liste est bien ordonnée par la date de publication (au cas où nous aurions des articles dont la publication ne s’est pas faite juste après l’écriture), il faudra utiliser la méthode OrderBy.

Cette dernière attend en argument une fonction qui retourne le paramètre utilisé pour ordonner.

Par exemple, pour nos articles, rangeons-les par ordre alphabétique de titre :

bdd.Articles.OrderBy(a => a.Titre).ToList();
La liste est ordonnée

Ainsi, avec la pagination, nous obtiendrons :

public ActionResult List(int page = 0)
        {
            try
            {
                //on saute un certain nombre d'article et on en prend la quantité voulue
                List<Article> liste = bdd.Articles
                                         .OrderBy(a => a.ID)
                                         .Skip(page * ARTICLEPERPAGE)
                                         .Take(ARTICLEPERPAGE).ToList();
                ViewBag.Page = page;
                return View(liste);
            }
            catch
            {
                return View(new List<Article>());
            }
        }
Nouvelle version de la pagination

Par exemple si nous avions 50 articles en base de données, avec 5 articles par page, nous afficherions à la page 4 les articles 21, 22, 23, 24 et 25.

Mais le plus beau dans tout ça, c’est qu’on peut chaîner autant qu’on veut les Skip et les Take. Ainsi en faisant : Skip(3).Take(5).Skip(1).Take(2) nous aurions eu le résultat suivant :

N° article Pris
1 non
2 non
3 non
4 oui
5 oui
6 oui
7 oui
8 oui
9 non
10 oui
11 oui

Si vous voulez que votre collection respecte un ordre spécial, il faudra lui préciser l’ordre avant de faire les Skip et Take, sinon le système ne sait pas comment faire.

Filtrer une liste

La méthode Where

Avant de filtrer notre liste, nous allons légèrement modifier notre entité Article et lui ajouter un paramètre booléen nommé EstPublie.

     public class Article
    {
        public Article()
        {
            EstPublie = true;//donne une valeur par défaut
        }
        public int ID { get; set; }
        [Column("Title")]
        [StringLength(128)]
        public string Titre { get; set; }
        [Column("Content")]
        public string Texte { get; set; }
        public string ThumbnailPath { get; set; }
        public bool EstPublie { get; set; }
    }
Ajout d’un attribut dans la classe Article

Maintenant, notre but sera de n’afficher que les articles qui sont marqués comme publiés.

Pour cela il faut utiliser la fonction Where qui prend en entrée un délégué qui a cette signature :

delegate bool Where(TEntity entity)
Signature de la méthode Where

Dans le cadre de notre liste, nous pourrions donc utiliser :

public ActionResult List(int page = 0)
        {
            try
            {
                //on saute un certain nombre d'articles et on en prend la quantité voulue
                List<Article> liste = bdd.Articles.Where(article => article.EstPublie)
                                         .Skip(page * ARTICLEPERPAGE).Take(ARTICLEPERPAGE).ToList();
                ViewBag.Page = page;
                return View(liste);
            }
            catch
            {
                return View(new List<Article>());
            }
        }
Filtrage de la liste en fonction de l’état de l’article

Dans vos filtres, toutes les fonctions ne sont pas utilisables dès qu’on traite les chaînes de caractères, les dates…

Filtre à plusieurs conditions

Pour mettre plusieurs conditions, vous pouvez utiliser les mêmes règles que pour les embranchements if et else if, c’est-à-dire utiliser par exemple article.EstPublie && article.Titre.Length < 100 ou bien article.EstPublie || !string.IsNullOrEmpty(article.ThumbnailPath).

Néanmoins, dans une optique de lisibilité, vous pouvez remplacer le && par une chaîne de Where.

List<Article> liste = bdd.Articles.Where(article => article.EstPublie)
                                  .Where(article => !string.IsNullOrEmpty(article.ThumbnailPath)`
Chaînage de Where

N’afficher qu’une seule entité

Afficher une liste d’articles, c’est bien, mais rapidement la liste deviendra longue. Surtout, nos articles étant des textes qui peuvent être assez denses, il est souhaitable que nous ayons une page où un article sera affiché seul.

Cette page vous facilitera aussi le partage de l’article : la liste est mouvante, mais la page où seul l’article est affiché restera toujours là, à la même adresse.

![[a]] | Il est vraiment important que la page reste à la même adresse et ce même si vous changez le texte ou le titre de l’article.

Pour le cours, cette page sera créée plus tard, lorsque nous verrons les liens entre les entités car nous voulons bien sûr afficher les commentaires de l’article sur cette page. Néanmoins, cela sera un bon exercice pour vous de la coder maintenant.

Indices

  • Pour sélectionner un article, il faut pouvoir l'identifier de manière unique.
  • La fonction db.VotreDBSet.FirstOrDefault(m=>m.Parametre == Valeur) vous permet d’avoir le premier objet qui vérifie la condition ou bien null si aucun article ne convient.
  • Par convention, en ASP.NET MVC on aime bien appeler cette page Details.

Ajouter une entité à la bdd

Quelques précisions

Avant de montrer le code qui vous permettra de sauvegarder vos entités dans la base de données, je voudrais vous montrer plus en détail comment Entity Framework gère les données.

Fonctionnement de EntityFramework
Fonctionnement de EntityFramework

Comme le montre le schéma, une entité n’est pas tout de suite envoyée à la base de données. En fait pour que cela soit fait il faut que vous demandiez à la base de données de persister tous les changements en attente.

"détachés" et "supprimés" sont des états qui se ressemblent beaucoup. En fait "détaché" cela signifie que la suppression de l’entité a été persistée.

À partir de là, le système va chercher parmi vos entités celles qui sont marquées :

  • créées ;
  • modifiées ;
  • supprimées.

Et il va mettre en place les actions adéquates.

La grande question sera donc :

Comment marquer les entités comme "créées" ?

Ajouter une entité

C’est l’action la plus simple de toutes : bdd.VotreDbSet.Add(votreEntite); et c’est tout !

Ce qui transforme notre code d’envoi d’article en :

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(ArticleCreation articleCreation)
        {
            if (!ModelState.IsValid)
            {
                return View(articleCreation);
            }

            string fileName = "";
            if (articleCreation.Image != null)
            {
                /* gestion de l'erreur pour l'image */
            }

            Article article = new Article
            {
                Contenu = articleCreation.Contenu,
                Pseudo = articleCreation.Pseudo,
                Titre = articleCreation.Titre,
                ImageName = fileName
            };

            bdd.Articles.Add(article);
            bdd.SaveChanges();

            return RedirectToAction("List", "Article");
        }
Persistance de l’article en base de données

Modifier et supprimer une entité dans la base de données

Préparation

Ici le cas sera plus "complexe" si je puis dire. En effet, pour s’assurer que quoi qu’il arrive le changement soit bien détecté sans perte de performance (c’est-à-dire qu’on ne va pas demander au système de comparer caractère par caractère le texte de l’article pour détecter le changement), il faudra forcer un peu les choses.

  1. Premièrement, il faudra aller chercher l’objet tel qu’il est stocké en base de données.
  2. Ensuite il faudra annoncer à EntityFramework "je vais le modifier".
  3. Enfin il faudra mettre en place les nouvelles valeurs.

Tout cela devra être mis dans la méthode "Edit" de votre contrôleur.

        [HttpGet]
        public ActionResult Edit(int id)
        {
            return View();
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(int id, ArticleCreation articleCreation)
        {
            return RedirectToAction("List");
        }
Les méthodes d’édition

Toutes les étapes sont importantes. Si vous ne faites pas la première, EF va possiblement croire que vous êtes en train de créer une nouvelle entité.

Je vous encourage à créer la vue Edit comme nous l’avions vu plus tôt. Il faudra aussi apporter quelques légères modifications à la vue List pour que la page d’édition soit accessible :

Trouvez les trois lignes suivantes :

                    @Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
                    @Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
                    @Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
Les liens incomplets

Et mettez-y l’ID de l’article :

                    @Html.ActionLink("Edit", "Edit", new {  id=item.ID  }) |
                    <!-- si vous n'avez pas créé la page Details dans l'exercice précédent, laissez inchangé-->
                    @Html.ActionLink("Details", "Details", new { id=item.ID  }) |
                    @Html.ActionLink("Delete", "Delete", new {  id=item.ID })
Les liens sont corrects.

Modifier l’entité

Pour trouver l’entité, vous pouvez utiliser la méthode Where comme vue précédemment. Le problème de cette méthode, c’est qu’elle retourne une collection, même si cette dernière ne contient qu’un élément.

Il vous faudra dès lors utiliser la méthode FirstOrDefault pour aller chercher l’entité.

Une alternative sera d’utiliser la méthode Find, c’est ce que nous ferons dans cette partie.

Une fois l’entité trouvée, il faut annoncer au contexte qu’elle est modifiée. C’est là que commence la partie "complexe" puisque pour s’assurer que la modification soit efficace et effective dans tous les cas, nous allons utiliser une capacité avancée de EntityFramework.

Le contexte de données contient une méthode Entry<TEntity>() qui permet de personnaliser la manière dont les entités sont gérées par Entity Framework.

Cette méthode retourne un objet de type DbEntityEntry qui a la possibilité de forcer l’état d’une entité, utiliser des fonctionnalité de différentiel (c’est-à-dire trouver la différence entre l’ancienne et la nouvelle version)…

Pour vous aider, voici le code :

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(int id, ArticleCreation articleCreation)
        {
            Article entity = bdd.Articles.Find(id);
            if (entity == null)
            {
                return RedirectToAction("List");
            }
            string fileName;
            if (!handleImage(articleCreation, out fileName))
            {

                return View(articleCreation);
            }
            DbEntityEntry<Article> entry = bdd.Entry(entity);
            entry.State = System.Data.Entity.EntityState.Modified;
            Article article = new Article
            {
                Contenu = articleCreation.Contenu,
                Pseudo = articleCreation.Pseudo,
                Titre = articleCreation.Titre,
                ImageName = fileName
            };
            entry.CurrentValues.SetValues(article);
            bdd.SaveChanges();

            return RedirectToAction("List");
        }
La méthode Edit

J’ai encapsulé la gestion de l’image dans une méthode pour que ça soit plus facile à utiliser.

        private bool handleImage(ArticleCreation articleCreation, out string fileName)
        {
            bool hasError = false;
            fileName = "";
            if (articleCreation.Image != null)
            {
                
                if (articleCreation.Image.ContentLength > 1024 * 1024)
                {
                    ModelState.AddModelError("Image", "Le fichier téléchargé est trop grand.");
                    hasError = true;
                }

                if (!AcceptedTypes.Contains(articleCreation.Image.ContentType)
                       || AcceptedExt.Contains(Path.GetExtension(articleCreation.Image.FileName).ToLower()))
                {
                    ModelState.AddModelError("Image", "Le fichier doit être une image.");
                    hasError = true;
                }

                try
                {
                    string fileNameFile = Path.GetFileName(articleCreation.Image.FileName);
                    fileName = new SlugHelper().GenerateSlug(fileNameFile);
                    string imagePath = Path.Combine(Server.MapPath("~/Content/Upload"), fileName);
                    articleCreation.Image.SaveAs(imagePath);
                }
                catch
                {
                    fileName = "";
                    ModelState.AddModelError("Image", "Erreur à l'enregistrement.");
                    hasError = true;
                }
                
            }
            return !hasError;
        }
gestion de l’image

La suppression de l’entité

La suppression se veut, bien heureusement plus simple.

Si l’utilisation de Entry est toujours possible, avec un simple changement d’état à Deleted, vous pouvez utiliser la méthode bdd.Articles.Remove qui prend en paramètre votre entité.

N’oubliez pas de sauvegarder les changements et c’est bon.

Par contre, je tenais à préciser une chose : la vue par défaut vous propose d’utiliser un lien et donc une requête GET. Il vaut mieux changer ça !

Utilisez une requête POST et assurez-vous d’être protégés contre la faille CSRF.

Je vous laisse le faire en exercice.

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Delete(int id)
        {
            Article a = bdd.Articles.Find(id);
            if (a == null)
            {
                return RedirectToAction("List");
            }
            bdd.Articles.Remove(a);
            bdd.SaveChanges();
            return RedirectToAction("List");
        }
le contrôleur de suppression
@using (Html.BeginForm("Delete", "Article", new { id = item.ID }, FormMethod.Post, null))
                    {
                        Html.AntiForgeryToken();
                        <button type="submit">Supprimer</button>
                    }
le code à changer dans la vue

Le patron de conception Repository

Le patron de conception Repository a pour but de remplacer la ligne ApplicationDbContext bdd = ApplicationDbContext.Create(); par une ligne plus "générique".

J’entends par là que nous allons créer une abstraction supplémentaire permettant de remplacer à la volée un ApplicationDbContext par autre chose ne dépendant pas d’entity Framework.

Tu nous apprends Entity Framework, mais tu veux le remplacer par autre chose, je ne comprends pas pourquoi !

En fait il existe des projets qui doivent pouvoir s’adapter à n’importe quel ORM, c’est rare, mais ça arrive.

Mais surtout, il faut bien comprendre qu’un site web, ça ne se développe pas n’importe comment.

En effet, si on simplifie beaucoup les choses, quand vous développez un site web, module par module, vous allez réfléchir en répétant inlassablement quatre étapes :

Méthode de développement d'un projet
Méthode de développement d'un projet

Premièrement, nous essayons de comprendre de quoi on a besoin. Jusqu’à maintenant, nous avons fait le travail pour vous. Par exemple, comme nous voulons créer un blog, nous vous avons dit "il faudra ajouter, modifier, supprimer des articles".

Le besoin est là, maintenant on va "concevoir". Là encore, nous vous avons bien dirigé jusqu’à présent. Par exemple, nous avons dit "un article est composé d’un texte, un titre, un auteur et éventuellement une icône".

Et là, vous avez commencé à coder. Et qu’avez-vous fait, pour voir que "ça marche" ? Eh bien vous avez testé.

N’avez-vous pas remarqué que c’est long et répétitif ?

Nous verrons plus tard dans le cours qu’il existe des outils qui testent automatiquement votre site. Mais comme ces tests ont pour but de vérifier que votre site fonctionne aussi bien quand l’utilisateur donne quelque chose de normal que des choses totalement hallucinantes, vous ne pouvez pas tester sur une base de données normale. Non seulement cela aurait pour effet de la polluer si les tests échouent, mais en plus cela ralentirait vos tests.

Alors on va passer par des astuces, que nous utiliseront plus tard mais qui elles n’utilisent pas EntityFramework.

C’est pourquoi nous allons vouloir limiter les liens entre les contrôleurs et EntityFramework. Nous allons créer un Repository1

En fait le but sera de créer une interface qui permettra de gérer les entités.

Par exemple, une telle interface peut ressembler à ça pour nos articles :

public interface EntityRepository<T> where T: class{
    IEnumerable<T> GetList(int skip = 0, int limit = 5);
    T Find(int id);
    void Save(T entity);
    void Delete(T entity);
    void Delete(int id);
}
L’interface commune à tous les Repository

Et comme on veut pouvoir utiliser EntityFramework quand même, on crée une classe qui implémente cette interface :

public class EFArticleRepository: EntityRepository<Article>{
    private ApplicationDbContext bdd = new ApplicationDbContext();
    public IEnumerable<Article> GetList(int skip = 0, int limit = 5){
        return bdd.Articles.OrderBy(a => a.ID).Skip(skip).Take(limit);
    }
    public Article Find(int id){
        return bdd.Articles.Find(id);
    }
    public void Save(Article entity){
        Article existing = bdd.Articles.Find(entity.id);
        if(existing == null){
           bdd.Articles.Add(entity);
        }else{
           bdd.Entry<Article>(existing).State = EntityState.Modified;
           bdd.Entry<Article>(existing).CurrentValues.SetValues(entity);
        }
        bdd.SaveChanges();
     }
    public void Delete(Article entity){
       bdd.Articles.Remove(entity);
    }
    public void Delete(int id){
        Delete(bdd.Articles.Find(id));
    }
}
Le Repository complet qui utilise Entity Framework

Dans la suite du cours, nous n’utiliserons pas ce pattern afin de réduire la quantité de code que vous devrez comprendre. Néanmoins, sachez que dès que vous devrez tester une application qui utilise une base de données, il faudra passer par là !


  1. Vous trouverez des renseignements complémentaires ici et ici.