I. Introduction▲
" Les ORM (Object Relational MappingObject 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 :
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
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
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
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
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
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
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
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.NetL'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.
III-A. Le Modèle▲
Ce projet contient une classe et une interface.
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.
" 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 !
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.
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 ".
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 disposableCA1001: 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.
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.
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.
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 }
);
}
}
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.
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.
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 "▲
D'après ce papier Improving Entity Framework PerformanceImproving 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 ".
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▲
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▲
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.
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.
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\n
Done..."
);
}
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\n
Wiping..."
);
mgr.
DropCreateTable
(
);
Console.
WriteLine
(
"
\r\n
Using 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.
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.
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 ".
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 ".
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 ".
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 ".
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 ".
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.
Le même schéma ci-dessous se reproduit quel que soit le nombre d'enregistrements.
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 ServiceSQL Server Integration Service ou encore SQL Server Service BrokerSQL 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 à ClaudeLELOUPClaudeLELOUP pour sa relecture. Merci à iberserkiberserk et Philippe VialattePhilippe Vialatte pour leur aide
VIII. Références▲
Vous trouverez le code source iciCode source. D'autres liens intéressants:
- Improving Entity Framework PerformanceImproving Entity Framework Performance ;
- Entity Framework : structurez et optimisez votre couche d'accès aux donnéesEntity Framework : structurez et optimisez votre couche d'accès aux données ;
- 10 Tips to Improve your LINQ to SQL Application Performance10 Tips to Improve your LINQ to SQL Application Performance ;
- CA1001: Types that own disposable fields should be disposableCA1001: Types that own disposable fields should be disposable.