Avec C# 2.0, il s’avérait extrêmement laborieux d’effectuer des jointures entre les DataTables d’un DataSet.

Dans ce genre de situation, il y a deux cas :

  • Soit on a des relations (System.Data.DataRelation) entre nos DataTables et on peut s’en sortir en utilisant les méthodes GetChildRows/GetParentRows au niveau d’une DataRow.
  • Soit on a pas des relations entre nos DataTables et là, on peut s’amuser à faire son petit manager maison.

Avec Linq apparu avec C# 3.0, vous allez tout de suite comprendre la simplicité qu’il en résulte en matière de jointure au niveau d’un DataSet.

Pour les exemples qui vont suivre, je vais m’appuyer sur un DataSet typé avec quelques DataTable générées à partir de la base de données Northwind de Microsoft. Mais rien ne vous empêchera de réaliser les exemples avec un DataSet non typé.

Il est important d’avoir à l’esprit que lors de l’utilisation de Linq avec des DataSets, Linq ne s’occupera en aucun cas du chargement des données, ce n’est pas son but contrairement à Linq to SQL. C’est donc à vous de vous assurer que les données sont chargées dans vos DataTables avant de commencer à effectuer des jointures.

Profitons en pour écrire notre méthode qui s’occupera de créer une instance de notre DataSet typé et de remplir les DataTable de données en provenance de la base de données SQL Server Northwind :

public dsNorthwind CreateDataSet()
{
    dsNorthwind northwind = new dsNorthwind();

    new DataSetJoin.dsNorthwindTableAdapters.CustomersTableAdapter().Fill(northwind.Customers);
    new DataSetJoin.dsNorthwindTableAdapters.OrdersTableAdapter().Fill(northwind.Orders);
    new DataSetJoin.dsNorthwindTableAdapters.Order_DetailsTableAdapter().Fill(northwind.Order_Details);
    new DataSetJoin.dsNorthwindTableAdapters.ProductsTableAdapter().Fill(northwind.Products);
    new DataSetJoin.dsNorthwindTableAdapters.CategoriesTableAdapter().Fill(northwind.Categories);

    return northwind;
}

Premier test de jointure

Allons-y doucement et essayons de récupérer la liste des commandes des clients :

public void Sample1()
{
    dsNorthwind northwind = CreateDataSet();

    var result = from order in northwind.Orders
                 from customers in northwind.Customers

                 select new { OrderId = order.OrderID, CustomerName = customers.ContactName };

    dataGridView1.DataSource = result.ToList();
}

Première tentative un peu ratée car on se retrouve entre produit cartésien entre les tables « Products » et « OrderDetails ».

On va donc définir le critère qui lie ces deux tables et qui est représenté par « CustomerID ».

public void Sample2()
{
    dsNorthwind northwind = CreateDataSet();

    var result = from order in northwind.Orders
                 from customers in northwind.Customers

                 where order.CustomerID == customers.CustomerID

                 select new { OrderId = order.OrderID, CustomerName = customers.ContactName };

    dataGridView1.DataSource = result.ToList();
}

Cependant rien empêche d’ajouter des critères supplémentaires comme par exemple la liste des clients ayant un « ContactName » commençant par ‘A’ :

public void Sample3()
{
    dsNorthwind northwind = CreateDataSet();

    var result = from order in northwind.Orders
                 from customers in northwind.Customers

                 where order.CustomerID == customers.CustomerID
                 && customers.ContactName.StartsWith(« A »)

                 select new { OrderId = order.OrderID, CustomerName = customers.ContactName };

    dataGridView1.DataSource = result.ToList();
}

Test de jointure normalisée

Malgré la possibilité de faire des jointures au niveau de la clause WHERE, il est pourtant déconseillé de procéder de la sorte pour diverses raisons :

  • Distinction difficile entre les critères de filtrage et la jointure en elle-même
  • Lisibilité pénible

C’est pourquoi SQL a introduit l’opérateur de jointure normalisé Sql2 « JOIN« . Heureusement pour nous, cet opérateur est disponible au sein de la syntaxe C# 3.0.

On va donc réécrire notre première requête qui est de récupérer la liste des commandes de nos clients mais cette fois en utilisant l’opérateur JOIN.

public void Sample4()
{
    dsNorthwind northwind = CreateDataSet();

    var result = from order in northwind.Orders
                 join customers in northwind.Customers
                 on order.CustomerID equals customers.CustomerID

                 select new { OrderId = order.OrderID, CustomerName = customers.ContactName };

    dataGridView1.DataSource = result.ToList();
}

Dans le cas d’une requête JOIN, on récupère uniquement les informations qui ont une correspondance dans les deux tables.

Le résultat de la jointure interne entre les clients et les commandes effectuées par les clients est représentée par l’intersection entre les deux tables. Sur l’image ci-dessus, la partie en orange.

Test de jointure externe

Tout comme dans une jointure interne, la jointure externe permet de récupérer comme résultat l’intersection de deux tables mais en plus, les informations d’une des deux tables dont les informations ne font pas partie de cette intersection.

Concrètement, si on prend comme exemple de récupérer la liste des produits faisant partie des commandes mais aussi ceux qui n’ont pas été utilisé dans des commandes, on appelle ça une jointure externe gauche.

A l’opposé, on peut vouloir récupérer la liste des produits faisant partie des commandes mais aussi les commandes qui n’ont pas de produits, on appelle ça une jointure externe droite.

Il n’y a hélas pas d’opérateur permettant d’effectuer des jointures externes (gauches ou droites). Cela ne va pourtant pas nous empêcher d’obtenir des résultats similaires car vous allez pouvoir constater que C# a tout ce qu’il faut pour réaliser ce type de jointure. L’exemple ci-dessous permet d’effectuer un jointure gauche en récupérant les produits qui n’ont jamais été utilisés dans une commande :

public void Sample5()
{
    dsNorthwind northwind = CreateDataSet();

    var result = from product in northwind.Products
                 join orderdetails in northwind.Order_Details
                 on product.ProductID equals orderdetails.ProductID
                 into productorderdetails
                 from pod in productorderdetails.DefaultIfEmpty()

                 where pod == null

                 select new { ProductName = product.ProductName};

    dataGridView1.DataSource = result.ToList();
}

La première partie de la requête Linq s’occupe de faire une jointure interne entre Products et OrderDetails :

…
from product in northwind.Products
join orderdetails in northwind.Order_Details
on product.ProductID equals orderdetails.ProductID
…

La seconde partie introduit un mot clé du langage C# dont nous n’avons pas encore parlé et qui est « into« . Comme l’explique MSDN, le mot clé « into » peut être utilisé pour définir un identificateur temporaire pour y stocker le résultat d’une clause group, join ou encore select. Dans le cas de l’utilisation de into avec join, on parle de jointure groupée, c’est à dire que le résultat obtenu par la jointure va être réorganisé par groupes. Chaque élément de la table source (donc gauche) sera associé à un tableau qui contient les éléments correspondants provenant de la table de droite. Dans le cas où un élement de la source n’a pas d’élément correspondant, il lui sera associé un tableau vide :

…
into productorderdetails
…

Maintenant que nous avons tous les éléments à notre disposition, nous allons pouvoir utiliser la méthode DefaultIfEmpty() sur notre identificateur « productorderdetails » afin de spécifier un élément de droite par défaut lorsque l’élément de gauche n’a pas de correspondance. Vous avez pu voir que je n’ai pas passé de paramètre à cette méthode pour dire que l’élément de droite est NULL mais rien ne vous empêche de spécifier un type par défaut :

…
from pod in productorderdetails.DefaultIfEmpty()
…

Enfin, on utilise la clause WHERE pour écarter les produits qui sont utilisés dans des commandes :

…
where pod == null
…

Et nous avons réalisé une jointure externe gauche avec Linq. Pour ce qui est de la jointure externe droite, comme vous vous en doutez déjà (sinon vous ne seriez pas là), il suffit de faire une jointure gauche en inversant les élément de la jointure.

Facile non ?