Certains ont sûrement déjà utilisés LINQ To SQL dans leur projet. L'un des principaux inconvénients lorsque l'on commence LINQ "sans savoir", c'est que l'on ne fait pas forcément attention au nombre de requêtes générées vers la base de données.
Il existe deux techniques de chargement des données depuis une base : le Lazy Loading (les données sont chargées à la demande) et l'Aggressive Loading (on récupère tout d'un coup).
Par défaut, LINQ To SQL utilise le premier mode de chargement des données. Pourtant, dans certains cas, il peut être très intéressant de limiter le nombre de requêtes à la base de données. Basons nous sur un exemple concret pour expliquer ces deux techniques.
Nous allons travailler avec la base de données Northwind et nous baser sur un DataContext plus que simple, puisque nous y ajoutons uniquement les tables Customer, Order et Order_Detail :
Cliquez pour agrandir
Dans un premier temps, et après avoir initialisé notre DataContext, nous allons tous simplement créer une requête LINQ To SQL qui va récupérer tous les "Customers" de la base de données. Nous allons aussi rediriger le Log du DataContext dans un fichier afin de récupérer les requêtes qui seront exécutées :
static void Main(string[] args)
{
NorthwindDataContext db = new NorthwindDataContext();
FileStream fs = new FileStream(@"C:\Test.sql", FileMode.OpenOrCreate, FileAccess.Write);
StreamWriter sr = new StreamWriter(fs);
db.Log = sr;
var customers = from c in db.Customers
select c;
}
Je vous rappel qu'aucune requête SQL n'a été effectuée pour le moment. En effet, comme je le souligne dans mon article Introduction à l'accès aux données avec Visual Studio 2008 et LINQ To SQL, les requêtes LINQ (en général, i.e pas uniquement LINQ To SQL) sont des requêtes dites à exécution différée, ou exécution tardive : elles ne sont exécutées que lorsque l'on souhaite parcourir / visualiser les résultats de ces dernières. Revenons à notre développement.
A présent, nous allons itérer sur notre variable "customers" afin d'afficher le nom de chaque client, mais aussi tous les achats et détails d'achat effectués par chaque client :
static void Main(string[] args)
{
NorthwindDataContext db = new NorthwindDataContext();
FileStream fs = new FileStream(@"C:\Test.sql", FileMode.OpenOrCreate, FileAccess.Write);
StreamWriter sr = new StreamWriter(fs);
db.Log = sr;
var customers = from c in db.Customers
select c;
foreach (var customer in customers)
{
Console.WriteLine(customer.ContactName);
Console.WriteLine("####################################################################");
foreach (var order in customer.Orders)
{
Console.WriteLine(order.OrderDate);
Console.WriteLine("######################################################");
foreach (var details in order.Order_Details)
{
Console.WriteLine(details.ProductID);
}
}
}
sr.Close();
fs.Close();
}
Exécutez le code et jettez un oeil au fichier SQL généré par les logs du DataContext :
On voit clairement que le nombre de requête est énorme. En effet, une première requête permet de récupérer les customers :
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address],
[t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
FROM [dbo].[Customers] AS [t0]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Puis l'on va récupérer, pour chaque customer les achats associés :
SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],
[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName],
[t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry]
FROM [dbo].[Orders] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [ALFKI]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Puis pour chaque achat, les détails associés :
SELECT [t0].[OrderID], [t0].[ProductID], [t0].[UnitPrice], [t0].[Quantity], [t0].[Discount]
FROM [dbo].[Order Details] AS [t0]
WHERE [t0].[OrderID] = @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [10643]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
SELECT [t0].[OrderID], [t0].[ProductID], [t0].[UnitPrice], [t0].[Quantity], [t0].[Discount]
FROM [dbo].[Order Details] AS [t0]
WHERE [t0].[OrderID] = @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [10692]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Nous sommes d'accord ici, ma requête est on ne peut plus simple et très peu travaillées (pas de jointure...). Bref une requête basique que nous avons tous écrit un jour ! Et derrière, un traitement assez lourd.
Voyons maintenant comment récupérer les mêmes informations, mais en n'ayant qu'une seule requête par client, ce qui va constituer la seconde méthode de chargement des données suscitée. Pour cela, nous allons introduire le concepte de DataLoadOptions.
Via cette classe du namespace System.Data.Linq, nous allons pouvoir choisir de précharger, entre autres, les EntityRef et EntitySet (représentation des relations 1-1 et 1-n d'une base de données au niveau du DBML) en même temps que l'objet LINQ sur lequel nous effectuons une requêtes.
Voilà alors ce que deviens notre code :
static void Main(string[] args)
{
NorthwindDataContext db = new NorthwindDataContext();
FileStream fs = new FileStream(@"C:\Test2.sql",FileMode.OpenOrCreate,FileAccess.Write);
StreamWriter sr = new StreamWriter(fs);
db.Log = sr;
DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Customer>(Customer => Customer.Orders);
options.LoadWith<Order>(Order => Order.Order_Details);
db.LoadOptions = options;
var customers = from c in db.Customers
select c;
foreach (var customer in customers)
{
Console.WriteLine(customer.ContactName);
Console.WriteLine("####################################################################");
foreach (var order in customer.Orders)
{
Console.WriteLine(order.OrderDate);
Console.WriteLine("######################################################");
foreach (var details in order.Order_Details)
{
Console.WriteLine(details.ProductID);
}
}
}
sr.Close();
fs.Close();
}
Ici, il faut prêter attention aux lignes suivantes :
DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Customer>(Customer => Customer.Orders);
options.LoadWith<Order>(Order => Order.Order_Details);
db.LoadOptions = options;
La première ligne nous permet d'instancier notre DataLoadOptions. Ensuite, nous précisons que nous souhaitons charger la propriété Orders de chaque Customer, mais aussi que nous souhaitons charger la propriété Order_Details de chaque Order.
Exécutons à présent notre petit programme est regardons le fichier SLQ généré. La première remarque à ce faire est la taille du fichier (fortement réduite comparée à précédement). Puis tout simplement les requêtes générées : il n'y en a qu'une seule pour chaque client et plus évoluée que précédement :
SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],
[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName],
[t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode],
[t0].[ShipCountry], [t1].[OrderID] AS [OrderID2], [t1].[ProductID], [t1].[UnitPrice],
[t1].[Quantity], [t1].[Discount], (
SELECT COUNT(*)
FROM [dbo].[Order Details] AS [t2]
WHERE [t2].[OrderID] = [t0].[OrderID]
) AS [value]
FROM [dbo].[Orders] AS [t0]
LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[OrderID] = [t0].[OrderID]
WHERE [t0].[CustomerID] = @x1
ORDER BY [t0].[OrderID], [t1].[ProductID]
-- @x1: Input NChar (Size = 5; Prec = 0; Scale = 0) [ALFKI]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Ainsi, les DataLoadOptions nous ont permis de réduire le nombre de requêtes générées vers la base de données en préchargeant directement les propriétés Orders et Order_Details au moment de la récupération de chaque Customer.
J'espère que ce petit exemple pourra éclairer vos lenternes sur cette partie du fonctionnement de LINQ To SQL.
Code source du projet : LINQ_and_DataLoadOptions.zip (71,98 kb)