La majorité des applications actuelles ont besoin d’enregistrer des informations dans une base de données locale ou serveur. Plusieurs outils existent dont Entity Framework, le plus fréquemment proposés par Microsoft, ou ADO.NET, le plus performant mais le plus complexe à exploiter. Depuis plusieurs années, nous avons construit un ensemble d’outils simples afin de nous aider dans la fabrication de la DAL de nos projets, et plus particulièrement dans la recherche de données en les transformant facilement en objets .NET.
Lors de cette session, nous expliquerons l’historique de ce toolkit en le comparant à quelques outils existants sur le marché, dont ADO.NET, Entity Framework ou Dapper.NET.
Ensuite, via des démos Live, je présenterai les fonctionnalités principales de SqlDatabaseCommand pour réaliser et optimiser les opérations courantes d’accès aux bases de données : récupérations de données typées, gestion des transactions, intégration de traces, gestion des exceptions, génération automatique de classe et l’injection de données pour les tests unitaires. Ces démos seront l’occasion de parler des bonnes pratiques d’accès aux bases de données de type SQL (SQL Server, SQLite, SQL Azure, etc).
Finalement, je montrerai comment utiliser ce toolkit dans des procédures CLR, intégrées à SQL Server. Ces procédures accroissent de manière phénoménale les performances de gestion des données.
6. 6
EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
7839 KING PRESIDENT 17-NOV-81 5000 10
7698 BLAKE MANAGER 7839 01-MAY-81 2850 30
7756 CLARK MANAGER 7839 09-JUN-81 1500 10
... ...
... ...
7456 JONES MANAGER 7839 02-APR-81 2975 20
DEPTNO DNAME LOC
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIOS BOSTON
7. 7
using (var connection = new SqlConnection(CONNECTION_STRING))
{
connection.Open();
connection.Close();
}
SELECT ENAME FROM EMP WHERE EMPNO = 7369
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT ENAME " +
" FROM EMP " +
" WHERE EMPNO = 7369 ";
}
using (var adapter = new SqlDataAdapter(cmd))
{
DataTable table = new DataTable();
adapter.Fill(table);
string name = table.Rows[0].Field<string>("ENAME");
}
8. 8
var db = new SCOTTEntities();
var query = from e in db.EMPs
where e.EMPNO == 7369
select e.ENAME;
var name = query.First();
var db = new SCOTTEntities();
var query = from e in db.EMPs
where e.EMPNO == 7369
select e;
var name = query.First().DEPT.DNAME; SELECT TOP (1)
[Extent1].[ENAME] AS [ENAME]
FROM [dbo].[EMP] AS [Extent1]
WHERE 7369 = [Extent1].[EMPNO]
SELECT TOP (1)
[Extent1].[EMPNO] AS [EMPNO],
[Extent1].[ENAME] AS [ENAME],
[Extent1].[JOB] AS [JOB],
[Extent1].[MGR] AS [MGR],
[Extent1].[HIREDATE] AS [HIREDATE],
[Extent1].[SAL] AS [SAL],
[Extent1].[COMM] AS [COMM],
[Extent1].[DEPTNO] AS [DEPTNO]
FROM [dbo].[EMP] AS [Extent1]
WHERE 7369 = [Extent1].[EMPNO]
SELECT
[Extent1].[DEPTNO] AS [DEPTNO],
[Extent1].[DNAME] AS [DNAME],
[Extent1].[LOC] AS [LOC]
FROM [dbo].[DEPT] AS [Extent1]
WHERE [Extent1].[DEPTNO] = @V1
12. 12
• Library that will extend IDbConnection
• Need an opened connection
using (var connection = new SqlConnection(CONNECTION_STRING))
{
connection.Open();
string sql = "SELECT * FROM EMP WHERE EMPNO = @Id";
var emp = connection.Query<EMP>(sql, new { Id = 7369 });
}
13. 13
string sql = "SELECT * FROM EMP WHERE EMPNO = @Id";
var emp = connection.Query<EMP>(sql, new { Id = 7369 });
string sql = "SELECT * FROM EMP WHERE EMPNO = @Id";
var emp = connection.Query(sql, new { Id = 7369 });
string sql = "SELECT ENAME FROM EMP WHERE EMPNO = @Id";
var emp = connection.ExecuteScalar<string>(sql, new { Id = 7369 });
var n = connection.Execute(“DELETE FROM EMP");
var emp = connection.Query(sql, buffered: false);
15. 15
• Sample
using (var cmd = new SqlDatabaseCommand(CONNECTION_STRING))
{
}
cmd.CommandText.AppendLine(" SELECT * ");
cmd.CommandText.AppendLine(" FROM EMP ");
var emps = cmd.ExecuteTable<Employee>();
cmd.CommandText.AppendLine(" WHERE HIREDATE = @HireDate ");
cmd.Parameters.AddValues(new
{
HireDate = new DateTime(1980, 12, 17)
});
16. 16
9876 NEW
• Main methods
EMPNO ENAME
7839 KING
7698 BLAKE
7756 CLARK
...
...
7456 JONES
var emps = cmd.ExecuteTable<Employee>();
• ExecuteTable
var smith = cmd.ExecuteRow<Employee>();
• ExecuteRow
var name = cmd.ExecuteScalar<String>();
• ExecuteScalar
var n = cmd.ExecuteNonQuery();
• ExecuteQuery
17. 17
• Parameters
cmd.CommandText.AppendLine(" SELECT ENAME ")
.AppendLine(" FROM EMP ")
.AppendLine(" WHERE EMPNO = @EmpNo ")
.AppendLine(" AND HIREDATE = @HireDate ");
cmd.Parameters.AddWithValue("@EmpNo", 7369);
cmd.Parameters.AddWithValue("@HireDate", new DateTime(1980, 12, 17));
var name = cmd.ExecuteScalar();
cmd.CommandText.AppendLine(" SELECT ENAME ")
.AppendLine(" FROM EMP ")
.AppendLine(" WHERE EMPNO = @EmpNo ")
.AppendLine(" AND HIREDATE = @HireDate ");
cmd.Parameters.AddValues(new
{
EmpNo = 7369,
HireDate = new DateTime(1980, 12, 17)
});
var name = cmd.ExecuteScalar();
18. 18
• Traces
cmd.Log = Console.WriteLine;
cmd.Log = (message) =>
{
Console.WriteLine(message);
};
string formatted = cmd.GetCommandTextFormatted(QueryFormat.Text);
SELECT ENAME
FROM EMP
WHERE EMPNO = 7369
AND HIREDATE = '1970-05-04 14:15:16'
string formatted = cmd.GetCommandTextFormatted(QueryFormat.Html);
SELECT ENAME
FROM EMP
WHERE EMPNO = 7369
AND HIREDATE = '1970-05-04 14:15:16'
19. 19
• Entities Generator
// *********************************************
// Code Generated with Apps72.Dev.Data.Generator
// *********************************************
using System;
namespace Data.Tests.Entities
{
/// <summary />
public partial class BONUS
{
/// <summary />
public virtual String ENAME { get; set; }
/// <summary />
public virtual String JOB { get; set; }
/// <summary />
public virtual Int32? SAL { get; set; }
/// <summary />
public virtual Int32? COMM { get; set; }
}
/// <summary />
public partial class DEPT
{
/// <summary />
public virtual Int32 DEPTNO { get; set; }
/// <summary />
public virtual String DNAME { get; set; }
/// <summary />
public virtual String LOC { get; set; }
}
var entitiesGenerator = new SqlEntitiesGenerator(CONNECTION_STRING);
foreach (var table in entitiesGenerator.Tables)
{
...
}
20. 20
• Best Practice
public class DataService : IDataService
{
public SqlDatabaseCommand GetDatabaseCommand()
{
return new SqlDatabaseCommand(CONNECTION_STRING);
}
public SqlDatabaseCommand GetDatabaseCommand(SqlTransaction trans)
{
return new SqlDatabaseCommand(trans.Connection, trans);
}
}
using (var cmd = service.GetDatabaseCommand())
{
...
}
21. 21
• Microsoft.Data.Sqlite
• SqliteDatabaseCommand
public SqliteDatabaseCommand GetDatabaseCommand()
{
return new SqliteDatabaseCommand("Filename=Scott.db");
}
public IEnumerable<Scott.EMP> GetAllEmployees()
{
using (var cmd = this.GetDatabaseCommand())
{
cmd.CommandText.AppendLine(" SELECT * FROM EMP ");
return cmd.ExecuteTable<Scott.EMP>();
}
}
25. 25
[SqlFunction(DataAccess = DataAccessKind.Read)]
public static int GetMaximumAge()
{
using (var cmd = new SqlDatabaseCommand("context connection=true"))
{
...
}
}
CREATE FUNCTION GetMaximumAge()
RETURNS INT
AS EXTERNAL NAME SampleSqlDatabaseCommandClr.SampleCLR.GetMaximumAge
Créer un projet Console.
Créer une chaine de CONNECTION_STRING = @"Server=(localdb)\ProjectsV12;Database=Scott;Integrated Security=true;“
Copier le code et l’executer.
Ouvrir SQL Server Profiler et choisir le modèle TSQL... et verifier la requête SQL qui y passe.Choisir uniquement RPC:Starting et SQL:BatchStarting dans le Profiler.
Ajouter une classe ADO.NET Entity Data Model « ScottEF »
Sélectionner EF Designer from database.
Choisir une chaine de connexion existante ou en créer une nouvelle vers (localdb)\ProjectsV12 et Scott
Enregistrer la connection dans App.Config: ScottEntities
Eventuellement, choisir Entity Framework 6.x
Cocher EMP et DEPT et Pluralize object names
Enregistrer le modèle sous ScottModel
Performance : EF est médiocre, comparé aux requêtes SQL, surtout sur de grand volumes de données.
Vitesse de développement : ADO trop long à écrireEF est trop complexe à comprendre et maitriser le framework.
Maintenabilité du code (code propre) : ADO est trop bas niveau et est trop complexe à écrire.EF demande le développement de procédures stockées, ce qui complexifies les débuggages.
FlexibilitéADO permet de construire les exactes requêtes SQL nécessaires.Pour EF, il n'est pas toujours évident de savoir ce qu'il se passe en coulisses, quelles requêtes sont effectivement exécutées sur la base de données, quelles données sont conservées en cache, dans quels cas le chargement tardif (lazy loading) s'applique, etc. Quand un bug lié à l'ORM se produit, il est parfois difficile de trouver son origine ;
EvolutivitéPour ADO, les changements dans le code peuvent être nombreux.Pour EF, les évolutions dans le framework et les outils associés sont fréquents et très difficiles à maintenir dans le temps.
1. Créer un nouveau projet Console.
2. Ajouter le Nuget Dapper.
3. Créer une simple requête et l’exécuter.
public class EMP
{
public Int32 EMPNO { get; set; }
public String ENAME { get; set; }
public String JOB { get; set; }
public Int32? MGR { get; set; }
public DateTime? HIREDATE { get; set; }
public Decimal? SAL { get; set; }
public Int32? COMM { get; set; }
public Int32? DEPTNO { get; set; }
}
Ajouter un nouveau fichier Text template.
Rechercher le fichier Entities.tt sur le site https://github.com/Apps72/Dev.Data
Copier / coller son contenu dans le fichier Entities.tt du point 1.
Vérifier que les propriétés du .tt sont Build Action = Content Custom Tool = TextTemplatingFileGenerator
Enregistrer le fichier.
Créer la classe DataService
Créer un exemple d’utilisation
static DataService service = new DataService();
public static void DisplaySmith()
{
Console.WriteLine();
Console.WriteLine("Best Practice");
using (var cmd = service.GetDatabaseCommand())
{
cmd.CommandText.AppendLine(" SELECT ENAME, DNAME ");
cmd.CommandText.AppendLine(" FROM EMP ");
cmd.CommandText.AppendLine(" INNER JOIN DEPT ON EMP.DEPTNO = DEPT.DEPTNO ");
cmd.CommandText.AppendLine(" WHERE EMPNO = @ID ");
cmd.Parameters.AddValues(new { ID = 7369 });
var emp = cmd.ExecuteRow(new { EName = "", DName = "" });
Console.WriteLine($"{emp.EName} - {emp.DName}");
}
}
3. Modifier le DataService pour gérer les traces et les erreurs.
public SqlDatabaseCommand GetDatabaseCommand()
{
var cmd = new SqlDatabaseCommand(CONNECTION_STRING);
cmd.Log = (message) =>
{
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine(message);
Console.ResetColor();
};
cmd.ExceptionOccured += (sender, e) =>
{
Console.WriteLine($"SQL ERROR: {e.Exception.Message}");
};
return cmd;
}
Créer un nouveau projet UWP (et le passer en x86).
Ajouter la référence Nuget (include prerelease)SqliteDatabaseCommandMicrosoft.Data.Sqlite
Importer les fichiers Scott.cs DataService.cs
Copier le code XAML et CodeBehind.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView x:Name="listView" Margin="10">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="10">
<Image Source="{Binding PICTURE}" Width="60" Height="60" />
<StackPanel Margin="20, 10">
<TextBlock Text="{Binding ENAME}" />
<TextBlock Text="{Binding JOB}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
public MainPage()
{
this.InitializeComponent();
DataService data = new DataService();
listView.ItemsSource = data.GetAllEmployees();
}
Créer une librairie C# en version 4.0
Ajouter le package Nuger SqlServerClr