Licence CC BY

Manipuler ses entités avec EF

Publié :

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.

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
 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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 :

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

Ainsi, avec la pagination, nous obtiendrons :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
     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 :

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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.

1
2
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

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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
        [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

Une erreur est survenue dans la génération de texte Markdown. Veuillez rapporter le bug.

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

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 :

1
2
3
4
5
6
7
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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