Mesure des performances de Linq to SQL face à Sql et Entity Framework

Ou comment tester les fournisseurs de données pour être certain de faire le choix qui correspond le mieux à vos attentes.

Immobilis

De nombreuses discussions ont lieu à propos de l'usage de " Linq to SQL ", beaucoup en critiquent les performances. Dans la mesure où j'essaie de fonder mes choix sur des arguments concrets, j'ai développé un petit programme pour tester différentes façons de réaliser les opérations C.R.U.D. (Create, Read, Update, Delete) à l'aide de " Linq to SQL ", SQL et Entity Framework. Commentez

- Titre : Mesure des performances de Linq to SQL face à Sql et Entity Framework

- Auteur : Immobilis

- Parution : 26 juin 2011

- Mise à jour : 1 juillet 2011

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2011 Immobilis. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. La base de données

- III. L'architecture

- III-A. Le Modèle

- III-A-1. L'entité "CustomerEntity"

- III-A-2. L'interface du C.R.U.D.

- III-B. La couche d'accès aux données

- III-B-1. Le fournisseur de données SQL

- III-B-2. Le fournisseur de données " Linq to SQL "

- III-B-3. Le fournisseur de données " Linq to SQL " utilisant les procédures stockées SQL liées dans le DataContext

- III-B-4. Le fournisseur de données " Linq to SQL " exécutant des commandes SQL au travers du DataContext

- III-B-5. Le fournisseur de données " Linq to Entities "

- III-B-6. Le fournisseur de données " Linq to Entities " utilisant des procédures stockées

- III-C. La couche métier

- III-D. Le programme Console de test

- IV. Tests et collecte de mesures

- IV-A. Test du fournisseur de données SQL

- IV-B. Test du fournisseur de données " Linq to SQL "

- IV-C. Test du fournisseur de données " Linq to SQL " utilisant les procédures stockées SQL liées dans le DataContext

- IV-D. Test du fournisseur de données " Linq to SQL " exécutant des commandes SQL au travers du DataContext

- IV-E. Test du fournisseur de données " Linq to Entities "

- IV-F. Test du fournisseur de données " Linq to Entities " utilisant des procédures stockées

- IV-G. Résultats globaux

- V. Courte analyse

- VI. Conclusion

- VII. Remerciements

- VIII. Références

I - Introduction

" Les ORM (Object Relational Mapping), c'est mal ". C'est en substance ce qu'on peut lire dans certaines discussions des forums. Mais pourquoi donc ? J'ai trouvé qu'assez souvent les réponses manquaient d'arguments solides. Parfois même, les réactions sont épidermiques.

Afin de me faire ma propre idée, j'ai développé ce petit programme dont je soumets les résultats à votre analyse. Cette application console permet de réaliser les opérations de création, lecture, mise à jour et suppression d'enregistrements. Grâce aux compteurs de performance de Windows et SQL Server Profiler, nous allons obtenir des mesures qui devraient nous permettre de décrypter le comportement de plusieurs fournisseurs de données.

J'espère que cette modeste contribution vous apportera des éléments de réponse à la question fatidique : devez-vous faire du SQL, du " Linq to SQL " ou du " Linq to Entities " ?

En avant!

II - La base de données

Voici ci-dessous le script SQL pour créer la table et les procédures stockées. Créez tout d'abord, une base de données nommée " Marketing ". Puis exécutez le script ci-dessous :

Script de génération de la table '' Customers ''

USE [Marketing]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Customers](
[Id] [uniqueidentifier] NOT NULL,
[LastName] [varchar](50) NOT NULL,
[FirstName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Customers] ADD  CONSTRAINT [DF_Customers_Id]  DEFAULT (newid()) FOR [Id]
GO

Script de génération de la procédure stockée '' CreateCustomersTable ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[CreateCustomersTable]
AS
BEGIN

CREATE TABLE [dbo].[Customers](
[Id] [uniqueidentifier] NOT NULL,
[LastName] [varchar](50) NOT NULL,
[FirstName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Customers] ADD  CONSTRAINT [DF_Customers_Id]  DEFAULT (newid()) FOR [Id]

END
GO

Script de génération de la procédure stockée '' DropCustomersTable ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[DropCustomersTable]
AS
BEGIN

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Customers]') AND type in (N'U'))
DROP TABLE [dbo].[Customers]

END
GO

Script de génération de la procédure stockée '' CreateCustomer ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[CreateCustomer]
@Id AS uniqueidentifier,
@LastName AS varchar(50),
@FirstName AS varchar(50)
AS
BEGIN
INSERT INTO [Marketing].[dbo].[Customers]
([Id]
,[LastName]
,[FirstName])
VALUES
(@Id, @LastName, @FirstName)

END
GO

Script de génération de la procédure stockée '' SelectCustomer ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[SelectCustomer]
@ID as uniqueidentifier
AS
BEGIN
SET NOCOUNT ON;
SELECT * from [Marketing].[dbo].[Customers] where Id = @ID

END
GO

Script de génération de la procédure stockée '' SelectAllCustomers ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[SelectAllCustomers]
AS
BEGIN
SET NOCOUNT ON;

SELECT * FROM [Marketing].[dbo].[Customers]
END
GO

Script de génération de la procédure stockée '' UpdateCustomer ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[UpdateCustomer]
@Id AS uniqueidentifier,
@LastName AS varchar(50),
@FirstName AS varchar(50)
AS
BEGIN
UPDATE [Marketing].[dbo].[Customers]
Set [LastName] = @LastName
,[FirstName] = @FirstName
where [Id] = @Id
END
GO

Script de génération de la procédure stockée '' DeleteCustomer ''

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[DeleteCustomer]
@ID as uniqueidentifier
AS
BEGIN
SET NOCOUNT ON;

delete from [Marketing].[dbo].[Customers] where [Id] = @ID
END
GO

III - L'architecture

Je vais utiliser une architecture multicouche. Celle-ci va nous permettre de facilement mettre en place plusieurs fournisseurs de données. Vous trouverez plus de détails à ce propos dans cet article : L'architecture multicouche mise en oeuvre sur une application Web ASP.Net. Voici d'ores et déjà un petit diagramme des classes et interfaces dont nous allons nous servir.

Liste des Objets

III-A - Le Modèle

Ce projet contient une classe et une interface.

Le projet Modèle de données

III-A-1 - L'entité "CustomerEntity"

Il s'agit donc d'un simple DTO représentant une entité " Client ". Il comporte trois propriétés :

- Id : identifiant ;

- LastName : nom de famille ;

- FirstName : prénom.

Entité ''Customer''

" Linq to SQL " crée des objets métiers dès qu'on ajoute une table au contexte de données. Dès lors, pourquoi créer une entité supplémentaire ? Et bien tout simplement pour affranchir l'architecture de la base de données. Ainsi, les couches supérieures de la DAL (logique métier, services, interface utilisateur, etc.) ne seront jamais liées à une base de données en particulier. Vous pourrez, si nécessaire, changer pour du MySQL ou de l'Oracle sans changer autre chose que l'implémentation votre fournisseur de données. Magique !

Code de l'entité ''CustomerEntity''

using System;

namespace Model
{
public class CustomerEntity
{
public Guid Id { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
}

III-A-2 - L'interface du C.R.U.D.

L'interface va définir les méthodes C.R.U.D. Elle est placée dans ce projet afin de pouvoir être implémentée sur les objets de la couche métier et de la couche d'accès aux données.

- Create : insère un enregistrement ;

- Read : retourne tous les enregistrements de la table ou un seul selon un identifiant ;

- Update : met à jour un enregistrement selon son Id ;

- Delete : supprime un enregistrement selon son Id.

Interface définissant les méthodes C.R.U.D.

Code de l'interface du C.R.U.D.

using System;
using System.Collections.Generic;

namespace Model
{
/// <summary>
/// Implements <c>IDisposable</c> in order to perform application-defined tasks associated
/// with freeing, releasing, or resetting unmanaged resources
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ICrud<T>: IDisposable
{
/// <summary>
/// Method used to insert a <c>T</c> object in the database.
/// </summary>
/// <param name="obj">The <c>T</c> object to insert.</param>
/// <returns>Integer: the unique id of the record.</returns>
Guid Create(T obj);

/// <summary>
/// Returns a <c>T</c> object from the database.
/// </summary>
/// <param name="obj">A <c>T</c> object with its unique identifier set.</param>
/// <returns>The requested <c>T</c> object.</returns>
T Read(T obj);

/// <summary>
/// Returns a <c>IEnumerable<T></c> object from the database.
/// </summary>
/// <returns>The requested <c>IEnumerable<T></c> object.</returns>
IEnumerable<T> Read();

/// <summary>
/// Updates a <c>T</c> object in the database.
/// </summary>
/// <param name="obj">The <c>T</c> object to update.</param>
/// <returns>True if success. False if fails.</returns>
bool Update(T obj);

/// <summary>
/// Deletes a <c>T</c> object in the database.
/// </summary>
/// <param name="obj">A <c>T</c> object with its unique identifier set.</param>
/// <returns>True if success. False if fails.</returns>
bool Delete(T obj);
}
}

Cette interface va apporter un touche de généricité. Grâce à elle vous pourrez constater la simplicité de la couche métier. C'est royal !

III-B - La couche d'accès aux données

Elle se compose de six classes (fournisseurs), d'un " DataContext " et d'un " Entity Data Model " nommés " Marketing ".

Projet Data Access Layer

Ces classes implémentent toutes " ICrud<T> ", en conséquence, elles implémentent aussi " IDisposable ". C'est indispensable pour ce mettre en conformité avec la bonne pratique suivante : CA1001: Types that own disposable fields should be disposable

III-B-1 - Le fournisseur de données SQL

Rien de plus classique que cette classe. J'ai mis quelques commentaires sur les méthodes. Avec ceux de l'interface, cela devrait suffire. Notez que cette classe dispose d'une méthode supplémentaire pour supprimer et recréer la table "[Customers]" entre chaque série de tests.

Le fournisseur de données SQL

Il faut noter que les méthodes ci-dessous ne gèrent pas l'éventualité d'une modification des enregistrements entre la première lecture et la mise à jour. Dans ce cas le dernier gagne.

Code du fournisseur de données SQL

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using Model;

namespace Dal
{
public class SqlCustomerProvider : ICrud<CustomerEntity>
{
/// <summary>
/// Instanciation de la connexion
/// </summary>
private SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MarketingConnectionString"].ToString());

private bool disposed = false;

/// <summary>
/// Constructeur ouvrant la connexion
/// </summary>
public SqlCustomerProvider()
{
conn.Open();
}

public Guid Create(CustomerEntity obj)
{
Guid g = Guid.NewGuid();
using (SqlTransaction trans = conn.BeginTransaction())
{
try
{
using (SqlCommand cmd = new SqlCommand("CreateCustomer", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@ID", g));
cmd.Parameters.Add(new SqlParameter("@LastName", obj.LastName));
cmd.Parameters.Add(new SqlParameter("@FirstName", obj.FirstName));
cmd.ExecuteNonQuery();
}
trans.Commit();
}
catch (SqlException e)
{
trans.Rollback();
throw e;
}
}
return g;
}

public CustomerEntity Read(CustomerEntity obj)
{
using (SqlCommand cmd = new SqlCommand("SelectCustomer", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@ID", obj.Id));
using (SqlDataReader rd = cmd.ExecuteReader())
{
while (rd.Read())
{
obj.LastName = rd["LastName"].ToString();
obj.FirstName = rd["FirstName"].ToString();
}
}
}
return obj;
}

public IEnumerable<CustomerEntity> Read()
{
List<CustomerEntity> list = new List<CustomerEntity>();

using (SqlCommand cmd = new SqlCommand("SelectAllCustomers", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
using (SqlDataReader rd = cmd.ExecuteReader())
{
while (rd.Read())
{
CustomerEntity c = new CustomerEntity();
c.Id = (Guid)rd["Id"];
c.LastName = rd["LastName"].ToString();
c.FirstName = rd["FirstName"].ToString();
list.Add(c);
}
}
}
return list;
}

public bool Update(CustomerEntity obj)
{
int i = 0;
using (SqlTransaction trans = conn.BeginTransaction())
{
try
{
using (SqlCommand cmd = new SqlCommand("UpdateCustomer", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@ID", obj.Id));
cmd.Parameters.Add(new SqlParameter("@LastName", obj.LastName));
cmd.Parameters.Add(new SqlParameter("@FirstName", obj.FirstName));
i = cmd.ExecuteNonQuery();
}
trans.Commit();
}
catch (SqlException e)
{
trans.Rollback();
throw e;
}
}
return Convert.ToBoolean(i);
}

public bool Delete(CustomerEntity obj)
{
int i = 0;
using (SqlTransaction trans = conn.BeginTransaction())
{
try
{
using (SqlCommand cmd = new SqlCommand("DeleteCustomer", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@ID", obj.Id));
i = cmd.ExecuteNonQuery();
}
trans.Commit();
}
catch (SqlException e)
{
trans.Rollback();
throw e;
}
}
return Convert.ToBoolean(i);
}

/// <summary>
/// Pour les besoins du test supprime et recréé la table
/// </summary>
public void DropCreateTable()
{
using (SqlTransaction trans = conn.BeginTransaction())
{
try
{
using (SqlCommand cmd = new SqlCommand("DropCustomersTable", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.ExecuteNonQuery();
}
using (SqlCommand cmd = new SqlCommand("CreateCustomersTable", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.ExecuteNonQuery();
}
trans.Commit();
}
catch (SqlException e)
{
trans.Rollback();
throw e;
}
}
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~SqlCustomerProvider()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
conn.Dispose();
}
}

#endregion
}
}

III-B-2 - Le fournisseur de données " Linq to SQL "

Le code de cette classe inclut une classe partielle du DataContext d'origine. C'est assez utile afin de propager nos méthodes compilées à tout le projet. Cela évite aussi de les perdre si on régénère le DataContext.

Le Marketing DataContext

Définition partielle du DataContext

namespace Dal
{
/// <summary>
/// Modification du <c>MarketingDataContext</c>. Ajout de deux méthodes permettant l'utilisation de deux requêtes compilées personnalisées.
/// </summary>
public partial class MarketingDataContext
{
public Customer GetCustomerById(Guid guid)
{
return fctGetCustomerById(this, guid);
}

public IQueryable<CustomerEntity> GetAllCustomers()
{
return fctGetAllCustomer(this);
}

/// <summary>
/// Requête compilée renvoyant un enregistrement de la table Customers
/// </summary>
private Func<MarketingDataContext, Guid, Customer> fctGetCustomerById = CompiledQuery.Compile(
(MarketingDataContext db, Guid guid) => (from c in db.Customers
where c.Id == guid
select c).SingleOrDefault());
/// <summary>
/// Requête compilée renvoyant la totalité de la table Customers. Les objets <c>Customer</c> sont directement convertis en <c>CustomerEntity</c>.
/// </summary>
private Func<MarketingDataContext, IQueryable<CustomerEntity>> fctGetAllCustomer = CompiledQuery.Compile(
(MarketingDataContext db) => from c in db.Customers
select new CustomerEntity() { Id = c.Id, LastName = c.LastName, FirstName = c.LastName });
}
}

Le fournisseur de données '' Linq to SQL ''

Code du fournisseur de données '' Linq to SQL ''

using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using Model;

// Placer ici le code de la classe partielle.

namespace Dal
{
public class LinqCustomerProvider : ICrud<CustomerEntity>
{
private MarketingDataContext db = new MarketingDataContext();
/// <summary>
/// Il est nécessaire de créer une nouvelle instance du contexte de données optimisé pour la lecture.
/// La propriété <c>ObjectTrackingEnabled</c> ne sera pas activée. Cela peut générer une erreur car
/// il n'est pas possible de modifier cette valeur si une requête a déjà été effectuée sur le <c>DataContext</c>.
/// </summary>
private MarketingDataContext dbRead = new MarketingDataContext();

private bool disposed = false;

public LinqCustomerProvider()
{
// Désactivation du suivi des objets.
dbRead.ObjectTrackingEnabled = false;
}

public Guid Create(CustomerEntity obj)
{
Customer c = new Customer() { Id = Guid.NewGuid(), LastName = obj.LastName, FirstName = obj.FirstName };
db.Customers.InsertOnSubmit(c);
db.SubmitChanges();
return c.Id;
}

public CustomerEntity Read(CustomerEntity obj)
{
Customer cust = dbRead.GetCustomerById(obj.Id);
obj.LastName = cust.LastName;
obj.FirstName = cust.FirstName;
return obj;
}

public IEnumerable<CustomerEntity> Read()
{
return dbRead.GetAllCustomers().ToList();
}

public bool Update(CustomerEntity obj)
{
Customer c = db.GetCustomerById(obj.Id);
c.LastName = obj.LastName;
c.FirstName = obj.FirstName;
db.SubmitChanges();
return true;
}

public bool Delete(CustomerEntity obj)
{
db.Customers.DeleteOnSubmit(db.GetCustomerById(obj.Id));
db.SubmitChanges();
return true;
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~LinqCustomerProvider()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
dbRead.Dispose();
}
}

#endregion
}
}

III-B-3 - Le fournisseur de données " Linq to SQL " utilisant les procédures stockées SQL liées dans le DataContext

Si votre base de données dispose de procédures stockées, ajoutez-les simplement en faisant un glisser-déplacer dans le DataContext. Affichez votre base de données dans l'explorateur de serveurs, naviguez jusqu'aux procédures stockées, prenez celles qui vous intéressent et déposez-les dans la fenêtre du DataContext.

Ajout des procédures stockées dans le DataContext

Le fournisseur de données '' Linq to SQL '' utilisant les procédures stockées SQL

Code du fournisseur de données '' Linq to SQL '' utilisant les procédures stockées SQL liées dans le DataContext

using System;
using System.Collections.Generic;
using System.Linq;
using Model;

namespace Dal
{
public class LinqSqlSPCustomerProvider : ICrud<CustomerEntity>
{
private MarketingDataContext db = new MarketingDataContext();
private bool disposed = false;

public Guid Create(CustomerEntity obj)
{
obj.Id = Guid.NewGuid();
db.CreateCustomer(obj.Id, obj.LastName, obj.FirstName);
return obj.Id;
}

public CustomerEntity Read(CustomerEntity obj)
{
var cust = db.SelectCustomer(obj.Id).ToList().FirstOrDefault();
obj.LastName = cust.LastName;
obj.FirstName = cust.FirstName;
return obj;
}

public IEnumerable<CustomerEntity> Read()
{
var customers = db.SelectAllCustomers().ToList();
List<CustomerEntity> list = new List<CustomerEntity>();
customers.ForEach(x => list.Add(new CustomerEntity() { Id = x.Id, LastName = x.LastName, FirstName = x.FirstName }));
return list;
}

public bool Update(CustomerEntity obj)
{
db.UpdateCustomer(obj.Id, obj.LastName, obj.FirstName);
return true;
}

public bool Delete(CustomerEntity obj)
{
db.DeleteCustomer(obj.Id);
return true;
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~LinqSqlSPCustomerProvider()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
}

#endregion
}
}

III-B-4 - Le fournisseur de données " Linq to SQL " exécutant des commandes SQL au travers du DataContext

Le DataContext permet d'envoyer des commandes SQL brutes.

Le fournisseur de données '' Linq to SQL '' utilisant les commandes SQL

Code du fournisseur de données '' Linq to SQL '' exécutant des commandes SQL au travers du DataContext

using System;
using System.Collections.Generic;
using System.Linq;
using Model;

namespace Dal
{
public class LinqSqlCmdCustomerProvider : ICrud<CustomerEntity>
{
private MarketingDataContext db = new MarketingDataContext();
private bool disposed = false;

public Guid Create(CustomerEntity obj)
{
obj.Id = Guid.NewGuid();
db.Customers.Context.ExecuteCommand(
"[CreateCustomer] @ID = {0}, @LastName = {1}, @FirstName = {2}",
obj.Id,
obj.LastName,
obj.FirstName
);
return obj.Id;
}

public CustomerEntity Read(CustomerEntity obj)
{
Customer c = db.Customers.Context.ExecuteQuery<Customer>("[SelectCustomer] @ID = {0}", obj.Id).SingleOrDefault();
obj.LastName = c.LastName;
obj.FirstName = c.FirstName;
return obj;
}

public IEnumerable<CustomerEntity> Read()
{
List<CustomerEntity> list = new List<CustomerEntity>();
db.Customers.Context.ExecuteQuery<Customer>("[SelectAllCustomers]").ToList().ForEach(
x => list.Add(new CustomerEntity() { Id = x.Id, LastName = x.LastName, FirstName = x.FirstName }));
return list;
}

public bool Update(CustomerEntity obj)
{
return Convert.ToBoolean(db.Customers.Context.ExecuteCommand(
"[UpdateCustomer] @ID = {0}, @LastName = {1}, @FirstName = {2}",
obj.Id,
obj.LastName,
obj.FirstName
));
}

public bool Delete(CustomerEntity obj)
{
return Convert.ToBoolean(db.Customers.Context.ExecuteCommand(
"[DeleteCustomer] @ID = {0}",
obj.Id));
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~LinqSqlCmdCustomerProvider()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
}

#endregion
}
}

III-B-5 - Le fournisseur de données " Linq to Entities "

Le fournisseur de données '' EntitiesCustomerProvider ''

D'après ce papier  Improving Entity Framework Performance, les performances de " Linq to Entities " sont améliorées si l'" EntityConnection " est déclarée statique. Cela permet d'éviter d'instancier cet objet chaque fois que vous créez une instance du fournisseur de données. Il faudrait vérifier que cela n'a pas de conséquences négatives et j'émettrai un petite réserve.
En effet, " EntityConnection " implémente l'interface " IDisposable ". A ce titre, et en vertue de la bonne pratique citée au début du paragraphe III-B, tout objet qui l'utilise doit lui-même implémenter l'interface " IDisposable ". Si je ne me trompe pas, cela impliquerait qu'il faut détruire l'objet " EntityConnection " quand l'objet qui l'utilise est lui-même détruit. Ceci permet de libérer les ressources. Du coup, il cela reviendrait au même que de ne pas la déclarer statique.
Enfin, il faudrait vérifier que cela est vraiment " Thread-safe ".

Le code du fournisseur de données '' Linq to Entities ''

using System;
using System.Collections.Generic;
using System.Data.EntityClient;
using System.Linq;
using Model;

namespace Dal
{
public class EntitiesCustomerProvider : ICrud<CustomerEntity>
{
private MarketingEntities db = null;
private EntityConnection conn = new EntityConnection("name=MarketingEntities");
private bool disposed = false;

public EntitiesCustomerProvider()
{
db = new MarketingEntities(conn);
}

public Guid Create(CustomerEntity obj)
{
Customers c = new Customers() { Id = Guid.NewGuid(), LastName = obj.LastName, FirstName = obj.FirstName };
db.Customers.AddObject(c);
db.SaveChanges();
return c.Id;
}

public CustomerEntity Read(CustomerEntity obj)
{
Customers c = db.Customers.Where(x => x.Id == obj.Id).SingleOrDefault();
CustomerEntity ce = new CustomerEntity() { Id = c.Id, LastName = c.LastName, FirstName = c.FirstName };
return ce;
}

public IEnumerable<CustomerEntity> Read()
{
List<CustomerEntity> list = new List<CustomerEntity>();
var query = db.Customers.AsEnumerable();
query.ToList().ForEach(x => list.Add(new CustomerEntity() { Id = x.Id, LastName = x.LastName, FirstName = x.FirstName }));
return list;
}

public bool Update(CustomerEntity obj)
{
db.Customers.ApplyCurrentValues(new Customers() { Id = obj.Id, LastName = obj.LastName, FirstName = obj.FirstName });
db.SaveChanges();
return true;
}

public bool Delete(CustomerEntity obj)
{
Customers c = db.Customers.Where(x => x.Id == obj.Id).SingleOrDefault();
db.Customers.DeleteObject(c);
db.SaveChanges();
return true;
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~EntitiesCustomerProvider()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
conn.Dispose();
db.Dispose();
}
}

#endregion
}
}

III-B-6 - Le fournisseur de données " Linq to Entities " utilisant des procédures stockées

Le fournisseur de données '' EntitiesSqlSPCustomerProvider ''

Code du fournisseur de données '' Linq to Entities '' utilisant des procédures stockées

using System;
using System.Collections.Generic;
using System.Data.EntityClient;
using System.Linq;
using Model;

namespace Dal
{
public class EntitiesSqlSPCustomerProvider : ICrud<CustomerEntity>
{
private MarketingEntities db = null;
private EntityConnection conn = new EntityConnection("name=MarketingEntities");
private bool disposed = false;

public EntitiesSqlSPCustomerProvider()
{
db = new MarketingEntities(conn);
}

public Guid Create(CustomerEntity obj)
{
obj.Id = Guid.NewGuid();
db.CreateCustomer(obj.Id, obj.LastName, obj.FirstName);
return obj.Id;
}

public CustomerEntity Read(CustomerEntity obj)
{
Customers c = db.SelectCustomer(obj.Id).SingleOrDefault();
CustomerEntity ce = new CustomerEntity() { Id = c.Id, LastName = c.LastName, FirstName = c.FirstName };
return ce;
}

public IEnumerable<CustomerEntity> Read()
{
List<CustomerEntity> list = new List<CustomerEntity>();
var query = db.SelectAllCustomers();
query.ToList().ForEach(x => list.Add(new CustomerEntity() { Id = x.Id, LastName = x.LastName, FirstName = x.FirstName }));
return list;
}

public bool Update(CustomerEntity obj)
{
return Convert.ToBoolean(db.UpdateCustomer(obj.Id, obj.LastName, obj.FirstName));
}

public bool Delete(CustomerEntity obj)
{
return Convert.ToBoolean(db.DeleteCustomer(obj.Id));
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~EntitiesSqlSPCustomerProvider()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
conn.Dispose();
db.Dispose();
}
}

#endregion
}
}

III-C - La couche métier

Le projet logique métier

Nous allons créer un " Manager " afin de piloter nos fournisseurs de données. L'interface " ICrud " va nous permettre d'ajouter une touche de généricité. Celle-ci nous évitera de dupliquer du code.

La classe métier '' CustomerManager ''

Code du '' CustomerManager ''

using System;
using System.Collections.Generic;
using Dal;
using Model;

namespace Bll
{
public enum DataProvider
{
Linq,
Sql,
LinqSqlCmd,
LinqSqlSP,
Entities,
EntitiesSqlSP
}

public class CustomerManager : ICrud<CustomerEntity>
{
// Utilisation de l'interface générique pour manipuler les fournisseurs
private ICrud<CustomerEntity> _provider = null;

private bool disposed = false;

public DataProvider DataProvider { get; private set; }

public CustomerManager(DataProvider dp)
{
DataProvider = dp;

switch (dp)
{
case DataProvider.Linq:
_provider = new LinqCustomerProvider();
break;
case DataProvider.Sql:
_provider = new SqlCustomerProvider();
break;
case DataProvider.LinqSqlCmd:
_provider = new LinqSqlCmdCustomerProvider();
break;
case DataProvider.LinqSqlSP:
_provider = new LinqSqlSPCustomerProvider();
break;
case DataProvider.Entities:
_provider = new EntitiesCustomerProvider();
break;
case DataProvider.EntitiesSqlSP:
_provider = new EntitiesSqlSPCustomerProvider();
break;
}
}

public Guid Create(CustomerEntity obj)
{
return _provider.Create(obj);
}

public CustomerEntity Read(CustomerEntity obj)
{
return _provider.Read(obj);
}

public IEnumerable<CustomerEntity> Read()
{
return _provider.Read();
}

public bool Update(CustomerEntity obj)
{
return _provider.Update(obj);
}

public bool Delete(CustomerEntity obj)
{
return _provider.Delete(obj);
}

/// <summary>
/// Petite méthode pour supprimer et recréer la table <c>Customers</c>
/// </summary>
public void DropCreateTable()
{
using (SqlCustomerProvider provider = new SqlCustomerProvider())
{
provider.DropCreateTable();
}
}

#region IDisposable

public void Dispose()
{
if (!disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
}
disposed = true;
}

~CustomerManager()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
_provider.Dispose();
}
}

#endregion
}
}

III-D - Le programme Console de test

Cette petite application va réaliser toutes les opérations du C.R.U.D. en utilisant tour à tour chacun des fournisseurs :

- suppression et création de la table " Customer " ;

- quatre passages d'insertion d'enregistrements avec des valeurs aléatoires ;

- plusieurs lectures de la totalité des enregistrements (la totalité de la table) ;

- une lecture de plusieurs enregistrements (un par un) ;

- mise à jour de la totalité des enregistrements un par un avec des valeurs aléatoires ;

- suppression de la totalité des enregistrements un par un.

Chaque opération sera chronométrée et entre chaque opération l'application fera une pause. Enfin les résultats seront enregistrés dans un fichier CSV.

L'application Console

Code de l'application Console

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Bll;
using Immobilis.ToolsBox.Security;
using Model;

namespace ConsoleApplication
{
class Program
{
private static Stopwatch sw = new Stopwatch();
private static string providerName = string.Empty;
private static List<string> infos = new List<string>();
private static CustomerEntity ce = new CustomerEntity();
private static List<CustomerEntity> list = new List<CustomerEntity>();
private static int writeNb = 2000;
private static int readNb = 1000;
private static int pauseLongue = 10000;
private static int pauseCourte = 3000;

static void Main(string[] args)
{
infos.Add("\"Provider\";\"Description\";\"Temps\";");
Execute(DataProvider.Sql);
Pause(pauseLongue);
Execute(DataProvider.Linq);
Pause(pauseLongue);
Execute(DataProvider.LinqSqlSP);
Pause(pauseLongue);
Execute(DataProvider.LinqSqlCmd);
Pause(pauseLongue);
Execute(DataProvider.Entities);
Pause(pauseLongue);
Execute(DataProvider.EntitiesSqlSP);
DumpToCsv();
Console.WriteLine("\r\nDone...");
}

private static void DumpToCsv()
{
using (StreamWriter sw = new StreamWriter("Result_" + providerName + "_" + DateTime.Now.ToString("hh-mm-ss") + ".csv", false, Encoding.UTF8))
{
string s = string.Empty;
foreach (var item in infos)
{
s += item + "\r\n";
}
sw.Write(s);
}
}

private static void Execute(DataProvider dp)
{
providerName = dp.ToString();
using (CustomerManager mgr = new CustomerManager(dp))
{
Console.WriteLine("\r\nWiping...");
mgr.DropCreateTable();
Console.WriteLine("\r\nUsing provider: {0}", mgr.DataProvider);
sw.Restart();
Create(mgr);
sw.Stop();
Pause(pauseCourte);
sw.Restart();
ReadAll(mgr);
sw.Stop();
infos.Add(string.Format("\"{0}\";\"Read {1} times {2} rows\";{3};", mgr.DataProvider, readNb, list.Count, sw.ElapsedMilliseconds));
Console.WriteLine("\t{0}", infos.Last());
Pause(pauseCourte);
sw.Restart();
ReadOneRandomly(mgr);
sw.Stop();
infos.Add(string.Format("\"{0}\";\"Read {1} rows\";{2};", mgr.DataProvider, readNb, sw.ElapsedMilliseconds));
Console.WriteLine("\t{0}", infos.Last());
Pause(pauseCourte);
sw.Restart();
Update(mgr);
sw.Stop();
infos.Add(string.Format("\"{0}\";\"Updated {1} rows\";{2};", mgr.DataProvider, list.Count, sw.ElapsedMilliseconds));
Console.WriteLine("\t{0}", infos.Last());
Pause(pauseCourte);
sw.Restart();
Delete(mgr);
sw.Stop();
infos.Add(string.Format("\"{0}\";\"Deleted {1} rows\";{2};", mgr.DataProvider, list.Count, sw.ElapsedMilliseconds));
Console.WriteLine("\t{0}", infos.Last());
}
}

private static void ReadOneRandomly(CustomerManager mgr)
{
for (int i = 0; i < readNb; i++)
{
ce = mgr.Read(list.ElementAt(list.Count - 1 - i));
}
}

private static void ReadAll(CustomerManager mgr)
{
for (int i = 0; i < readNb; i++)
{
list = mgr.Read().ToList();
}
}

private static void Pause(int time)
{
Console.WriteLine("Waiting {0}s...", time / 1000);
Thread.Sleep(time);
}

private static void Create(CustomerManager mgr)
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < writeNb; j++)
{
ce.LastName = Encryption.GenerateRandomString(15);
ce.FirstName = Encryption.GenerateRandomString(20);
mgr.Create(ce);
}
sw.Stop();
infos.Add(string.Format("\"{0}\";\"temps de création de {1} lignes\";{2};", mgr.DataProvider, writeNb, sw.ElapsedMilliseconds));
Console.WriteLine("\t{0}", infos.Last());
sw.Restart();
}
}

private static void Update(CustomerManager mgr)
{
foreach (var item in list)
{
item.LastName = Encryption.GenerateRandomString(15);
mgr.Update(item);
}
}

private static void Delete(CustomerManager mgr)
{
foreach (var item in list)
{
mgr.Delete(item);
}
}
}
}

IV - Tests et collecte de mesures

L'ordinateur sur lequel sont réalisés les tests est un portable MEDION MD96464, Intel(R) Core(TM)2 Duo, CPU T7500 @ 2.20GHz, 2 Go de RAM, Vista 32 bits, SQL Server 2008 R2, Visual Studio 2010.

Médion MD 96464

Nous utiliserons les compteurs de performance de Windows et de SQL Server Profiler. Les résultats obtenus sont à relativiser impérativement. Cette application tourne sur mon portable ! Dans un véritable environnement de production (serveurs séparés, multicoeur, multidisque, etc.) les résultats seraient un peu différents. L'intérêt de ce test est surtout de dégager une tendance.

Le test consistera en :

- l'insertion de 4 x 2000 enregistrements ;

- mille lectures de la totalité de la table ;

- une lecture de 1000 enregistrements différents un par un ;

- la mise à jour de 8000 enregistrements un par un ;

- la suppression de 8000 enregistrements un par un.

Pour chaque fournisseur de données nous nous intéresserons :

- au texte des requêtes exécutées par le serveur ;

- aux valeurs des compteurs de performance du groupe " General Statistics " :

- Connection Reset/sec ;

- Logical Connections ;

- Logins/sec ;

- Logouts/sec ;

- Transactions ;

- User Connections.

- Au temps d'exécution.

IV-A - Test du fournisseur de données SQL

Opération

Texte de la requête

Création

exec CreateCustomer @ID='3558DE05-99B3-4B93-B2E9-37C225EB0C05',@LastName=N'jkgUJT5UdJIAsZV',@FirstName=N'jkgUJT5UdJIAsZVhlRyV'

Sélection de tous

exec SelectAllCustomers

Sélection de 1

exec SelectCustomer @ID='8D835206-EC15-4CCF-9C78-F83A87B9FFF0'

Mise à jour

exec UpdateCustomer @ID='B822486C-E0A9-4910-B39D-0310B2CC5A00',@LastName=N'eWNLOmMD3mPPfSG',@FirstName=N'WnNCkEKlTvelUdP0GMi2'

Suppression de 1

exec DeleteCustomer @ID='B822486C-E0A9-4910-B39D-0310B2CC5A00'

L'utilisation d'objets SQL donne le contrôle total du code sur les requêtes. Seul ce qui a été codé est exécuté par la base de données.

Compteurs de performances du fournisseur SQL

Une transaction, deux connexions utilisateur et logique, etc. l'activité du serveur SQL correspond exactement aux requêtes envoyées par le code.

IV-B - Test du fournisseur de données " Linq to SQL "

Opération

Texte de la requête

Création

exec sp_executesql N'INSERT INTO [dbo].[Customers]([Id], [LastName], [FirstName]) VALUES (@p0, @p1, @p2)',N'@p0 uniqueidentifier,@p1 varchar(8000),@p2 varchar(8000)' ,@p0='0105EFFD-1862-4155-8FFF-49A922D17912',@p1='3wccLoo5dYHNYAc',@p2='3wccLoo5dYHNYAc9BtB7'
go
exec sp_reset_connection
go

Sélection de tous

SELECT [t0].[Id], [t0].[LastName], [t0].[FirstName] FROM [dbo].[Customers] AS [t0] (exécution sous la forme d'un batch, ndlr)
exec sp_reset_connection

Sélection de 1

exec sp_executesql N'SELECT [t0].[Id], [t0].[LastName], [t0].[FirstName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Id] = @p0',N'@p0 uniqueidentifier',@p0='F5DD2BA3-5A0A-46DA-8129-F29DCA99092D'
go
exec sp_reset_connection
go

Mise à jour

exec sp_executesql N'SELECT [t0].[Id], [t0].[LastName], [t0].[FirstName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Id] = @p0',N'@p0 uniqueidentifier',@p0='F6757E88-B91A-4F1B-8BC6-2E0D48FA1585'
go
exec sp_reset_connection
go
exec sp_executesql N'UPDATE [dbo].[Customers] SET [LastName] = @p3, [FirstName] = @p4 WHERE ([Id] = @p0) AND ([LastName] = @p1) AND ([FirstName] = @p2)', N'@p0 uniqueidentifier,@p1 varchar(8000),@p2 varchar(8000),@p3 varchar(8000),@p4 varchar(8000)', @p0='F6757E88-B91A-4F1B-8BC6-2E0D48FA1585',@p1='m9YoagK0jbGnt1J',@p2='m9YoagK0jbGnt1JrWlal', @p3='USi3F1auqJpzRwL',@p4='m9YoagK0jbGnt1J'
go
exec sp_reset_connection
go

Suppression de 1

exec sp_executesql N'SELECT [t0].[Id], [t0].[LastName], [t0].[FirstName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Id] = @p0',N'@p0 uniqueidentifier',@p0='F5DD2BA3-5A0A-46DA-8129-F29DCA99092D'
go
exec sp_reset_connection
go
exec sp_executesql N'DELETE FROM [dbo].[Customers] WHERE ([Id] = @p0) AND ([LastName] = @p1) AND ([FirstName] = @p2)',N'@p0 uniqueidentifier,@p1 varchar(8000),@p2 varchar(8000)', @p0='F5DD2BA3-5A0A-46DA-8129-F29DCA99092D',@p1='x3LB0ux4CMBUe6H',@p2='dfMpavAYZUwqlMSACWDT'
go
exec sp_reset_connection
go

On remarque immédiatement qu'il se passe beaucoup plus de choses :

- la requête Linq a été retranscrite en une requête paramétrée ;

- les paramètres ne sont pas adaptés à la taille des champs de la table ;

- elle est exécutée grâce à la procédure stockée " sp_executesql " ;

- les requêtes aboutissant à la modification d'enregistrements induisent une lecture préalable ;

- chaque requête est suivie de l'exécution de la procédure stockée " sp_reset_connection ".

Compteurs de performances du fournisseur '' Linq to SQL ''

Sachant qu'il y a beaucoup plus de requêtes envoyées au serveur, le temps d'exécution du cycle est évidemment beaucoup plus long.

IV-C - Test du fournisseur de données " Linq to SQL " utilisant les procédures stockées SQL liées dans le DataContext

Opération

Texte de la requête

Création

declare @p6 int
set @p6=0
exec sp_executesql N'EXEC @RETURN_VALUE = [dbo].[CreateCustomer] @Id = @p0, @LastName = @p1, @FirstName = @p2',N'@p0 uniqueidentifier,@p1 varchar(8000),@p2 varchar(8000), @RETURN_VALUE int output',@p0='812419EE-F704-4932-B869-87123EB3B49C',@p1='rVp2HEouehQtdGc',@p2='rVp2HEouehQtdGcRIbJU', @RETURN_VALUE=@p6 output
select @p6
go
exec sp_reset_connection
go

Sélection de tous

declare @p3 int
set @p3=0
exec sp_executesql N'EXEC @RETURN_VALUE = [dbo].[SelectAllCustomers] ', N'@RETURN_VALUE int output',@RETURN_VALUE=@p3 output
select @p3
go
exec sp_reset_connection
go

Sélection de 1

declare @p4 int
set @p4=0
exec sp_executesql N'EXEC @RETURN_VALUE = [dbo].[SelectCustomer] @ID = @p0',N'@p0 uniqueidentifier,@RETURN_VALUE int output',@p0='ADB2CFEF-013D-438C-947F-F18ED7B4BCD7', @RETURN_VALUE=@p4 output
select @p4
go
exec sp_reset_connection
go

Mise à jour

declare @p6 int
set @p6=0
exec sp_executesql N'EXEC @RETURN_VALUE = [dbo].[UpdateCustomer] @Id = @p0, @LastName = @p1, @FirstName = @p2',N'@p0 uniqueidentifier,@p1 varchar(8000),@p2 varchar(8000), @RETURN_VALUE int output',@p0='8973DA85-2F78-4E4C-B1C7-2590D35C1208',@p1='Gy74Aq8u1HhRwrH',@p2='307ggIVZwfiJdJ3nmE9u', @RETURN_VALUE=@p6 output
select @p6
go
exec sp_reset_connection
go

Suppression de 1

declare @p4 int
set @p4=0
exec sp_executesql N'EXEC @RETURN_VALUE = [dbo].[DeleteCustomer] @ID = @p0',N'@p0 uniqueidentifier,@RETURN_VALUE int output',@p0='2E07BDA6-66F2-428F-809E-1882C78BD51D', @RETURN_VALUE=@p4 output
select @p4
go
exec sp_reset_connection
go

Contrairement à l'utilisation pure de " Linq to SQL ", l'appel aux procédures stockées n'implique plus de sélection préalable. Le Framework construit une requête SQL avec des déclarations de variables. " Linq to SQL " fait toujours appel à la procédure stockée " sp_reset_connection ".

Compteurs de performances du fournisseur '' Linq to SQL '' utilisant les procédures stockées SQL

La durée d'exécution est légèrement plus longue que pour le fournisseur SQL. Les pics de réinitialisation des connexions (compteur " Connection Reset/sec ") sont bien visibles.

IV-D - Test du fournisseur de données " Linq to SQL " exécutant des commandes SQL au travers du DataContext

Opération

Texte de la requête

Création

exec sp_executesql N'[CreateCustomer] @ID = @p0, @LastName = @p1, @FirstName = @p2', N'@p0 uniqueidentifier,@p1 nvarchar(4000),@p2 nvarchar(4000)',@p0='F0E6C455-96A2-4B7F-8B30-91FD45F92CFD', @p1=N'9N9ZlC50hE0Srs2',@p2=N'PZ1zcAeqskJbNRkVu2bS'
go
exec sp_reset_connection
go

Sélection de tous

[SelectAllCustomers] (exécution sous la forme d'un batch, ndlr)
exec sp_reset_connection

Sélection de 1

exec sp_executesql N'[SelectCustomer] @ID = @p0',N'@p0 uniqueidentifier', @p0='69DD33C6-8EC5-4853-8FDB-CBEEB22FB82D'
go
exec sp_reset_connection
go

Mise à jour

exec sp_executesql N'[UpdateCustomer] @ID = @p0, @LastName = @p1, @FirstName = @p2', N'@p0 uniqueidentifier,@p1 nvarchar(4000),@p2 nvarchar(4000)',@p0='A38A905F-6E06-4EC4-9134-2B56327A9654', @p1=N'ezia7v5fSz3Sv9T',@p2=N'SY6rKoiLXvCuYHFqMvrE'
go
exec sp_reset_connection
go

Suppression de 1

exec sp_executesql N'[DeleteCustomer] @ID = @p0',N'@p0 uniqueidentifier', @p0='55FB2793-80A5-4C82-9664-7E5C57ACD1B3'
go
exec sp_reset_connection
go

La requête exécutée par une commande est réinterprétée par le Framework qui déclare des paramètres. " Linq to SQL " fait toujours appel à la procédure stockée " sp_reset_connection ".

Compteurs de performances du fournisseur '' Linq to SQL '' utilisant des commandes SQL

Idem que précédemment, la durée d'exécution est légèrement plus longue que pour le fournisseur SQL. Les pics de réinitialisation des connexions (compteur " Connection Reset/sec ") sont bien visibles.

IV-E - Test du fournisseur de données " Linq to Entities "

Opération

Texte de la requête

Création

exec sp_executesql N'insert [dbo].[Customers]([Id], [LastName], [FirstName]) values (@0, @1, @2) ',N'@0 uniqueidentifier,@1 varchar(50),@2 varchar(50)',@0='63716843-0523-43DB-814D-720F0EA89D55', @1='7pL7ob8pmrWDC0k',@2='7pL7ob8pmrWDC0krI0a5'
go
exec sp_reset_connection
go

Sélection de tous

SELECT [Extent1].[Id] AS [Id], [Extent1].[LastName] AS [LastName], [Extent1].[FirstName] AS [FirstName] FROM [dbo].[Customers] AS [Extent1] (exécution sous la forme d'un batch, ndlr)
go
exec sp_reset_connection
go

Sélection de 1

exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[LastName] AS [LastName], [Extent1].[FirstName] AS [FirstName] FROM [dbo].[Customers] AS [Extent1] WHERE [Extent1].[Id] = @p__linq__0', N'@p__linq__0 uniqueidentifier',@p__linq__0='C0081E77-DAC4-4DEE-8CD8-EE49105C0EF1'
go
exec sp_reset_connection
go

Mise à jour

exec sp_executesql N'update [dbo].[Customers] set [LastName] = @0 where ([Id] = @1) ', N'@0 varchar(50),@1 uniqueidentifier',@0='H4zdsEWZ6KLMwXC',@1='13B235C0-35B8-48B1-8557-1E21048A4C16'
go
exec sp_reset_connection
go

Suppression de 1

exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[LastName] AS [LastName], [Extent1].[FirstName] AS [FirstName] FROM [dbo].[Customers] AS [Extent1] WHERE [Extent1].[Id] = @p__linq__0', N'@p__linq__0 uniqueidentifier',@p__linq__0='C436C8AF-5EDD-4F1A-815D-4FA88CEADC2E'
go
exec sp_reset_connection
go
exec sp_executesql N'delete [dbo].[Customers] where ([Id] = @0)',N'@0 uniqueidentifier', @0='C436C8AF-5EDD-4F1A-815D-4FA88CEADC2E'
go
exec sp_reset_connection
go

Avec " Linq to Entities " les requêtes exécutées sont proches de celles du fournisseur de données " Linq to SQL ".

Compteurs de performances du fournisseur '' Linq to Entities ''

On remarque que la durée d'exécution est plus longue. Cela est dû à l'attachement de l'enregistrement avant sa suppression. Par ailleurs, le nombre de connexions logique et utilisateur est plus important que pour les autres fournisseurs.

IV-F - Test du fournisseur de données " Linq to Entities " utilisant des procédures stockées

Opération

Texte de la requête

Création

exec [dbo].[CreateCustomer] @Id='E70A41CC-CC1C-4328-B24C-DA02038B6929', @LastName='lpPdOXNPPov8Qg5',@FirstName='lpPdOXNPPov8Qg5MLDVy'
go
exec sp_reset_connection
go

Sélection de tous

exec [dbo].[SelectAllCustomers]
go
exec sp_reset_connection
go

Sélection de 1

exec [dbo].[SelectCustomer] @ID='B967E767-EDC5-4F98-B3DF-FD0FAE0590FF'
go
exec sp_reset_connection
go

Mise à jour

exec [dbo].[UpdateCustomer] @Id='C6355134-EAA0-49DB-ADEA-E7E619B43511', @LastName='n5Ij4QkzGTuaaSE',@FirstName='28OF5YWnUVsK6iIIAwLv'
go
exec sp_reset_connection
go

Suppression de 1

exec [dbo].[DeleteCustomer] @ID='B02EE842-066A-4115-B113-DABFD29B8E8A'
go
exec sp_reset_connection
go

Le texte des requêtes est identique à celui du fournisseur SQL. " Linq to Entities " ajoute un appel à la procédure stockée " sp_reset_connection ".

Compteurs de performances du fournisseur '' Linq to Entities '' utilisant des procédures stockées

La durée d'exécution est légèrement plus longue que pour le fournisseur SQL. Les pics de réinitialisation des connexions (compteur " Connection Reset/sec ") sont bien visibles. On remarque une fois de plus que le nombre de connexions logique et utilisateur est plus important que pour les autres fournisseurs.

IV-G - Résultats globaux

Dans le tableau ci-dessous se trouve la moyenne des temps en millisecondes de deux cycles de tests de tous les fournisseurs l'un après l'autre sans arrêt. Ces mesures sont récupérées à partir du fichier csv. L'opération de lecture d'un enregistrement est relativement longue. Cela est dû à la première exécution qui nécessite des opérations supplémentaires de la part du code et de la base de données. Les opérations suivantes sont de l'ordre de la milliseconde.

Performances comparées de tous les fournisseurs

Le même schéma ci-dessous se reproduit quel que soit le nombre d'enregistrements.

Performances comparées de tous les fournisseurs

V - Courte analyse

Premièrement, incontestablement, les requêtes rédigées en " Linq to SQL " sont les moins performantes. On peut clairement se dire que les requêtes " Linq to SQL ", qu'elles soient compilées ou non ne devraient pas être utilisées pour réaliser des opérations de masse. Dans les autres cas, ça se discute.

En effet, il faut prendre en compte le comportement du DataContext, qui met en place par défaut le suivi des objets. Ce comportement est utile lors des modifications concurrentes. Il est rare que ce processus soit pris en charge (développé par les ingénieurs) avec les objets SQL.

Deuxièmement, d'après les tests, les fournisseurs " Linq + SP ", " Linq + Cmd " et " Linq to Entities " sont sur certains points plus rapides que le Sql traditionnel. Cela mériterait toutefois d'être testé plus en profondeur dans un environnement de production.

Troisièmement, comparée aux objets SQL, la simplicité de la syntaxe de " Linq to SQL " ou " Linq to Entities " est un énorme avantage. Ceci est d'autant plus vrai que les performances sont tout à fait similaires. On notera :

- l'ouverture et la fermeture des connexions ;

- l'utilisation de transactions ;

- l'utilisation d'objets fortement typés. Avec SQL, on récupère un DataReader qu'il faut convertir ;

- la disposition des objets ;

- l'appel de la procédure " sp_reset_connection ".

Quatrièmement, au regard des requêtes SQL générées par " Linq to SQL " ou " Linq to Entities ", le Framework gèrerait lui-même un certain nombre d'opérations supplémentaires. Cet effet se retrouve aussi dans " Linq to Entities ". Ces opérations sont-elles utiles ? Ne seraient-elles pas de de bonnes pratiques que nous aurions tendance à oublier ? Par exemple, la gestion des accès concurrents. Dès lors, les temps de traitements sont forcement allongés.

VI - Conclusion

Ce tutoriel n'a d'autre but que de comparer les performances de SQL, " Linq to SQL " et " Linq to Entities ". Pour ma part, il met en évidence qu'il faut réfléchir avant de coder. Si vous voulez faire de la mise à jour en masse utilisez Microsoft SQL Server Integration Service ou encore SQL Server Service Broker. Si vous voulez mapper des objets entre votre base de données et votre code utilisez " Linq to SQL " ou " Entity Framework " avec des procédures stockées. Si vous n'avez pas le temps de coder une procédure stockée, faites une requête compilée ou utilisez une commande au travers du DataContext.

Finalement, ne dites pas que telle ou telle technologie est mieux ou moins bien, n'accusez pas le DBA (quand il y en a un) ou le développeur qui vient de terminer son BTS ou encore l'ingénieur qui sort de son école même si c'est un . Considérez les cas d'utilisation, les performances, la qualité du code. Aidez-vous les uns les autres, offrez un café à votre DBA, faites-lui un et vous verrez, tout ira mieux .

VII - Remerciements

Merci à ClaudeLELOUP pour sa relecture. Merci à iberserk et Philippe Vialatte pour leur aide

VIII - Références

Vous trouverez le code source ici. D'autres liens intéressants:

- Improving Entity Framework Performance ;

- Entity Framework : structurez et optimisez votre couche d'accès aux données ;

- 10 Tips to Improve your LINQ to SQL Application Performance ;

- CA1001: Types that own disposable fields should be disposable.