Pagination, ordre et désordre : les classes

Prenons le schéma suivant, dans la continuité de ce billet (utilise le composite-id pour du legacy) :

Je veux obtenir la liste des associations gérées par un responsable, on a une méthode qui ira nous les chercher :

/// <summary>
        ///     Liste des Associations pour un responsable
        /// </summary>
        /// <param name="respid">id responsable</param>        
        /// <param name="paginationOrderParameters">pagination et ordre</param>
       /// <returns>liste Assoc</returns>
        List<Assoc> GetAssocForAResponsable(int respid, PaginationOrderParameters paginationOrderParameters);

PaginationOrderParameters permettra de préciser quelle page et combien d'éléments il nous faut. Egalement, si l'on souhaite avoir un tri particulier, on pourra le préciser à l'aide d'une liste de propriétés à trier. Enfin, paginationOrderParameters sera garni (CountTotal) du nombre total d'éléments trouvés, hors pagination.

Cela donne la classe suivante :

public class PaginationOrderParameters 
    {
        /// <summary>
        ///     objet pagination
        /// </summary>
        public Pagination Pagination { get; set; }
        /// <summary>
        ///     liste des propriétés éventuelles à trier
        /// </summary>
        public List<OrderField> OrderFields { get; set; }
    }
 
    public class Pagination 
    {
        /// <summary>
        ///     le nombre d'éléments d'une page
        /// </summary>
        public int PageSize { get; set; }
        /// <summary>
        ///     la page courante, de 0 à n
        /// </summary>
        public int PageIndex { get; set; }
        /// <summary>
        ///     nombre total d'éléments trouvés (count), hors pagination
        /// </summary>
        public int CountTotal { get; set; }
    }
 
    public class OrderField 
    {
        /// <summary>
        ///     true : asc, false : desc
        /// </summary>
        public bool Ascending { get; set; }
        /// <summary>
        ///     la propriété touchée par le tri
        /// </summary>
        public string Field { get; set; }
    }

Code

Voyons le code de la méthode qui va interroger, avec NHibernate, la base. Parmi les nombreux avantages d'un ORM, il y a celui de s'affranchir de coder du SQL, qui peut être très verbeux dans le cas de la pagination côté SQL.

On utilisera les méthodes ci-après, elles feront tout le boulot ou presque à notre place. Le dialect MSSQL >= 2005 générera le SQL de pagination pour l'interrogation en base selon la syntaxe du serveur utilisé (SQLite : limit, offset, SQL Server : TOP, ROW_NUMBER).

  • SetFirstResult : début des éléments à prendre, correspond à l'usage du ROW_NUMBER et au découpage par page. On s'appuie sur paginationOrderParameters : SetFirstResult(PageIndex * PageSize)
  • SetMaxResults : correspond au TOP, on s'appuie là aussi sur paginationOrderParameters : SetMaxResults(PageSize)
  • Order : pour le tri

NB : jusqu'au dialect MSSQL 2000, le framework utilisait uniquement le TOP du SQL, le reste étant réalisé par NH : TOP n puis découpe des éléments ramenés par NH. Avec les nouveaux dialects (MSSQL 2005 et 2008), NH utilise le ROW_NUMBER() qui permet d'effectuer de la pagination bien plus efficacement.

List<Assoc> IAssocManager.GetAssocForAResponsable(int respid, PaginationOrderParameters paginationOrderParameters)
{
    var where = new [] { Restrictions.Eq("Responsable", respid) };
 
    // subquery
    var assocCriteria = DetachedCriteria.For<ResponsableAssoc>().SetProjection(Projections.Distinct(Projections.Property("Assoc")));
 
    foreach (var w in where)            
        assocCriteria.Add(w);
 
    // les assocs
    var critabo = _laSession.CreateCriteria(typeof (Assoc), "assoc")
        .Add(Subqueries.PropertyIn("Id", assocCriteria));
 
    // le nb. d'assocs
    var countabos = _laSession.CreateCriteria<Assoc>();            
 
    countabos.Add(Subqueries.PropertyIn("Id", assocCriteria));           
    countabos.SetProjection(Projections.RowCount());
 
    // la pagination et le tri
    if (paginationOrderParameters != null)
    {
        // Tri
        if (paginationOrderParameters.OrderFields != null && paginationOrderParameters.OrderFields.Count > 0)
        {
            foreach (var orderField in paginationOrderParameters.OrderFields)
            {
                var myfield = orderField.Field.Split(new[] { '.' });
                if (myfield.Length > 1)
                    if (critabo.GetCriteriaByAlias(myfield[0]) == null)
                        critabo.CreateAlias(myfield[0], myfield[0]);
 
                critabo.AddOrder(new Order(orderField.Field, orderField.Ascending));
            }
        }
        // Pagination                
        if (paginationOrderParameters.Pagination != null)
            critabo
                .SetFirstResult(paginationOrderParameters.Pagination.PageIndex < 0 ? 0 : paginationOrderParameters.Pagination.PageIndex * paginationOrderParameters.Pagination.PageSize)
                .SetMaxResults(paginationOrderParameters.Pagination.PageSize);
 
        if (paginationOrderParameters.Pagination != null)
            paginationOrderParameters.Pagination.CountTotal = countabos.FutureValue<int>().Value;
    }
 
    return critabo.Future<Assoc>().ToList();
}

Testons tout ça

[Test]
public void GetAssocForAResponsable_with_pagination_page1_should_return_2assocs()
{
    var paginationOrderParameters = new PaginationOrderParameters{Pagination =new Pagination { PageIndex = 0, PageSize = 2 }};
 
    var results = assocmgr.GetAssocForAResponsable(1972, paginationOrderParameters);
 
    Assert.AreEqual(2, results.Count);
    Assert.AreEqual(3, paginationOrderParameters.Pagination.CountTotal);
}
 
[Test]
public void GetAssocForAResponsable_with_pagination_page2_should_return_1assocs()
{
    var paginationOrderParameters = new PaginationOrderParameters {Pagination =new Pagination{PageIndex = 1, PageSize = 2}};
 
    var results = assocmgr.GetAssocForAResponsable(1972, paginationOrderParameters);            
 
    Assert.AreEqual(1,results.Count);
    Assert.AreEqual(3,paginationOrderParameters.Pagination.CountTotal);
}
 
[Test]
public void GetAssocForAResponsable_with_pagination_page2_should_return_1assoc_orderby_code_desc()
{
    var paginationOrderParameters = new PaginationOrderParameters { 
        Pagination = new Pagination { PageIndex = 1, PageSize = 2 },
        OrderFields = new List<OrderField> {new OrderField{Field = "Code", Ascending = false}}
    };
 
    var results = assocmgr.GetAssocForAResponsable(1972, paginationOrderParameters);
 
    Assert.AreEqual(1, results.Count);
    Assert.AreEqual("A01", results[0].Code);
    Assert.AreEqual(3, paginationOrderParameters.Pagination.CountTotal);
}
 
[Test]
public void GetAssocForAResponsable_with_pagination_page1_should_return_1assoc_orderby_code_asc()
{            
    var paginationOrderParameters = new PaginationOrderParameters
    {
        Pagination = new Pagination { PageIndex = 0, PageSize = 2 },
        OrderFields = new List<OrderField> { new OrderField { Field = "Code" } }
    };
 
    var results = assocmgr.GetAssocForAResponsable(1972, paginationOrderParameters);
 
    Assert.AreEqual(2, results.Count);
    Assert.AreEqual("B9078", results[1].Code);
    Assert.AreEqual(3, paginationOrderParameters.Pagination.CountTotal);
}

et hop ça fonctionne.

contexte Web

On pourra fabriquer un contrôle qui nous affichera la pagination côté utilisateur (avance, retour, affichage des n° pages) et gérera également le n° de page courant et le nombre de pages. Le nombre d'éléments, où tout est calculé à partir de celui-ci, est issu de paginationOrderParameters.Pagination.CountTotal, rempli lors de l'appel à la méthode.

On s'appuiera pour tout ça sur PagedDataSource.

Future et FutureValue

Les méthodes d'extension Future<T> ou FutureValue<T> permettent d'optimiser les allers et venues entre votre application et la base de données : plusieurs requêtes peuvent être envoyées en un seul appel, et idem pour le retour, tous les résultats seront ramenés en un seul retour. Dans notre exemple, cela va servir à ramener les associations ainsi que le nombre total en base en un seul aller-retour au lieu de 2.

En fait, cette fonctionnalité existait déjà dans les précédentes versions, plus connue sous le petit nom des MultiCriteria / MultiQueries, mais c'était moins bien fait. Je laisse Ayende l'expliquer bien mieux que moi.

Sources

Tout est dans le trunk , l'exemple se trouve sur AssocExemple.