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.
Date de publication : 26 juin 2011. Date de mise à jour : 1er juillet 2011.
Par
Immobilis (accueil)
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.
60 commentaires
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>
<c></c>
</summary>
<typeparam name="T"></typeparam>
public interface ICrud<T>: IDisposable
{
<summary>
<c></c>
</summary>
<param name="obj"><c></c></param>
<returns></returns>
Guid Create(T obj);
<summary>
<c></c>
</summary>
<param name="obj"><c></c></param>
<returns><c></c></returns>
T Read(T obj);
<summary>
<c><T></c>
</summary>
<returns><c><T></c></returns>
IEnumerable<T> Read();
<summary>
<c></c>
</summary>
<param name="obj"><c></c></param>
<returns></returns>
bool Update(T obj);
<summary>
<c></c>
</summary>
<param name="obj"><c></c></param>
<returns></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
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>
</summary>
private SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MarketingConnectionString"].ToString());
private bool disposed = false;
<summary>
</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>
</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>
<c></c>
</summary>
public partial class MarketingDataContext
{
public Customer GetCustomerById(Guid guid)
{
return fctGetCustomerById(this, guid);
}
public IQueryable<CustomerEntity> GetAllCustomers()
{
return fctGetAllCustomer(this);
}
<summary>
</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>
<c></c><c></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;
namespace Dal
{
public class LinqCustomerProvider : ICrud<CustomerEntity>
{
private MarketingDataContext db = new MarketingDataContext();
<summary>
<c></c>
<c></c>
</summary>
private MarketingDataContext dbRead = new MarketingDataContext();
private bool disposed = false;
public LinqCustomerProvider()
{
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>
{
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>
<c></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
VIII. Références
Vous trouverez le code source
ici.
D'autres liens intéressants:


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.
Cette page est déposée.