Sortir le statut de lecture et l'auteur de la table Livre
Deux changements de modele en une seule migration, plus la recherche qui en depend. Le statut de lecture devient personnel. Il etait une colonne de Livre, donc partage par tout le foyer, alors que deux membres lisent le meme exemplaire a des rythmes differents. Il vit desormais dans une table (LivreId, Utilisateur, Statut) avec unicite sur le couple. L'absence de ligne vaut « non commence » : sur une bibliotheque de foyer la plupart des couples n'ont aucun statut, et les materialiser tous multiplierait les lignes par le nombre de comptes pour n'exprimer qu'un vide. Rien n'est donc ecrit a la creation d'un livre. Piege rencontre : un Dictionary<int, Statut> renvoyait la valeur 0 de l'enumeration — « À lire » — pour un livre sans ligne, rendant « non commence » indiscernable d'un choix explicite. Le dictionnaire est desormais typé Statut?. L'auteur devient une table. Deux formes normalisees y cohabitent, et ce n'est pas une redondance : NomNormalise garde l'ordre de saisie pour la recherche en sous-chaine, CleRegroupement trie les mots et porte l'index unique, donc l'invariant « un auteur, une fiche ». Les initiales echappent a la cle et sont traitees en memoire, sur une table qui compte au plus quelques centaines de lignes. Un livre peut avoir plusieurs auteurs — le lookup ISBN en renvoie quatre pour Introduction to Algorithms — d'ou la table de liaison, avec une position qui conserve l'ordre de la couverture. Les rapprochements ambigus ne sont jamais appliques seuls : l'API les liste, l'utilisateur accepte ou refuse, et les refus sont memorises pour que la suggestion ne revienne pas. Le couple refuse est range par identifiant croissant, donc un refus vaut dans les deux sens. Reprise des donnees existantes. L'ancien statut, commun, est rattache a AjoutePar — seule personne que la base associe au livre. Les statuts des livres sans AjoutePar sont perdus : les attribuer serait une invention. Les trois valeurs sont reprises telles quelles, « À lire » compris, parce que c'est ce que l'ancienne interface affichait. L'ancien champ auteur devient une fiche par valeur distincte. La migration ne peut pas tout faire : lower() de SQLite ne retire pas les accents, donc « Émile Zola » et « emile zola » y restent deux fiches. ServiceRenormalisation finit le travail en C# au demarrage, reunit ces variantes, applique aussi la regle des initiales — sans quoi une base heritee resterait eclatee la ou une saisie neuve aurait ete reunie d'emblee — et garde le nom d'affichage le plus presentable. Il est idempotent, et sert de filet si les regles de normalisation changent. L'ordre de la migration compte : les colonnes condamnees sont recopiees dans une table de transit avant d'etre supprimees, parce que supprimer une colonne sous SQLite reconstruit la table. Verifie sur une base a l'ancien schema contenant 9 livres, 2 prets et trois variantes de Zola : prets intacts, statuts rattaches, les trois Zola reunis sous « Émile Zola », « P.F. Hamilton » absorbe par « Peter F. Hamilton », « Hamilton » seul laisse en suggestion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using MaBibli.Shared.Entites;
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Shared.Catalogue;
|
||||
|
||||
@@ -9,14 +10,21 @@ namespace MaBibli.Shared.Catalogue;
|
||||
/// Écrit sur <see cref="IQueryable{T}"/> pour être exécuté par EF Core côté base <b>et</b>
|
||||
/// testable en mémoire sur une simple liste, sans base ni réseau.
|
||||
/// <para>
|
||||
/// La comparaison passe par <c>ToLower()</c> : EF Core la traduit en SQL, et LINQ-to-Objects
|
||||
/// lui donne le même comportement insensible à la casse. Les accents ne sont pas normalisés
|
||||
/// (« Emile » ne trouve pas « Émile ») — limitation assumée à ce stade.
|
||||
/// La recherche porte sur les colonnes <b>normalisées</b> (<see cref="Livre.TitreNormalise"/>,
|
||||
/// <see cref="Auteur.NomNormalise"/>), pas sur les colonnes d'affichage : SQLite ne sait pas
|
||||
/// ignorer les accents, et sa collation <c>NOCASE</c> s'arrête à l'ASCII. Le terme cherché passe
|
||||
/// par la même normalisation que la valeur stockée — c'est ce qui fait que « emile » trouve
|
||||
/// « Émile ».
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class FiltreLivres
|
||||
{
|
||||
public static IQueryable<Livre> Appliquer(IQueryable<Livre> source, CritereLivres criteres)
|
||||
/// <param name="utilisateur">
|
||||
/// Personne dont on regarde le statut. Sert <b>uniquement</b> à
|
||||
/// <see cref="CritereLivres.Statut"/> : il ne restreint jamais la liste elle-même.
|
||||
/// </param>
|
||||
public static IQueryable<Livre> Appliquer(
|
||||
IQueryable<Livre> source, CritereLivres criteres, string? utilisateur)
|
||||
{
|
||||
// AUCUN filtre sur AjoutePar : la bibliothèque est commune (CLAUDE.md).
|
||||
// Ce commentaire est là pour qu'une régression future saute aux yeux en relecture.
|
||||
@@ -28,16 +36,32 @@ public static class FiltreLivres
|
||||
|
||||
if (criteres.Statut is { } statut)
|
||||
{
|
||||
source = source.Where(l => l.Statut == statut);
|
||||
// Le statut vit dans une table par personne : on interroge la ligne de l'appelant,
|
||||
// et lui seulement. Sans utilisateur identifié, personne n'a de statut : la sélection
|
||||
// est vide, ce qui vaut mieux que de montrer celui d'un autre.
|
||||
source = utilisateur is null
|
||||
? source.Where(_ => false)
|
||||
: source.Where(l => l.Statuts.Any(s => s.Utilisateur == utilisateur && s.Statut == statut));
|
||||
}
|
||||
|
||||
var terme = criteres.Recherche?.Trim();
|
||||
if (!string.IsNullOrEmpty(terme))
|
||||
if (criteres.AuteurId is { } auteurId)
|
||||
{
|
||||
var recherche = terme.ToLower();
|
||||
source = source.Where(l => l.Auteurs.Any(la => la.AuteurId == auteurId));
|
||||
}
|
||||
|
||||
var terme = NormalisationTexte.Normaliser(criteres.Recherche);
|
||||
if (terme.Length > 0)
|
||||
{
|
||||
// Le terme est confronté aux deux formes stockées de chaque auteur : le nom dans
|
||||
// l'ordre de saisie, et la clé aux mots triés. La seconde est ce qui fait que
|
||||
// « zola émile » trouve « Émile Zola » — le terme est trié de la même façon.
|
||||
var cle = RapprochementAuteurs.Cle(criteres.Recherche);
|
||||
|
||||
source = source.Where(l =>
|
||||
l.Titre.ToLower().Contains(recherche)
|
||||
|| (l.Auteur != null && l.Auteur.ToLower().Contains(recherche)));
|
||||
l.TitreNormalise.Contains(terme)
|
||||
|| l.Auteurs.Any(la =>
|
||||
la.Auteur!.NomNormalise.Contains(terme)
|
||||
|| la.Auteur.CleRegroupement.Contains(cle)));
|
||||
}
|
||||
|
||||
return Trier(source);
|
||||
@@ -49,7 +73,11 @@ public static class FiltreLivres
|
||||
/// <remarks>
|
||||
/// CLAUDE.md ne tranche pas le tri. L'ordre alphabétique a été retenu parce qu'une
|
||||
/// bibliothèque se parcourt par titre ; l'ordre d'ajout n'a d'intérêt que le jour de la saisie.
|
||||
/// <para>
|
||||
/// Le tri porte sur le titre normalisé : sinon « Œuvres » et « Éducation » atterrissent
|
||||
/// après « Zola » dans l'ordre des points de code.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IQueryable<Livre> Trier(IQueryable<Livre> source) =>
|
||||
source.OrderBy(l => l.Titre).ThenBy(l => l.Id);
|
||||
source.OrderBy(l => l.TitreNormalise).ThenBy(l => l.Id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user