Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

EF : la voie de la productivité

Une des principales forces de l’Entity Framework est le gain de productivité pour le développeur. Couplé avec le template de génération de code T4, ce gain explose. Imaginons que l’on veuille développer un service WCF exposant des données. Pour chaque type d’entité, vous allez très probablement vouloir faire un Get retournant la liste des entités, peut-être un deuxième Get prenant en paramètre l’id et chargeant l’entité avec ses relations, un Add, un Update, peut-être un Delete.

EF apporte un gain de productivité très important dans le développement des entités ainsi que dans l’utilisation de celles-ci. Cependant, dans notre cas, le code à écrire est particulièrement redondant : il faut écrire quasiment la même chose pour chacune des entités. Et c’est là que le T4 rentre en jeu.

Dans l’ensemble des exemples que j’ai pu regarder, ce template n’est utilisé que pour la génération d’entités. Essayons d’aller plus loin.

Grâce au template T4, vous allez écrire un meta-code qui va générer pour vous votre service WCF ! Et le mieux dans tout ça c’est que votre template est ensuite réutilisable. Pour ma part, je me suis amusé à faire un test de productivité entre un code utilisant l’ADO .NET 2.0 et un code utilisant l’Entity Framework. Première constatation : j’avais oublié que c’était si long de faire de l’ADO “classique”.

Avec EF et T4, la seule chose que je n’ai pas généré dans mon template est la liste des relations à charger avec l’entité (j’aurais pu utiliser une profondeur fixe avec T4 mais je voulais garder une logique métier pour le chargement des relations). Avec ADO .NET 2.0, il faut tout écrire !

De plus, qui dit ADO “classique” implique requête sous la forme d’une chaîne de caractères implique possibilité de faute de frappe dans les requêtes SQL implique des tests unitaires ce qui prend du temps.

Un des premiers points que l’on constate c’est l’apport en visibilité du code avec EF :

Pour la méthode GetOrder(orderId), le code ADO 2.0 (un peu factorisé) est le suivant :

private const string SELECT_ORDERS = "OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate, ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion, ShipPostalCode, ShipCountry FROM ORDERS";

private const string SELECT_ORDERDETAILS = "OrderID, ProductID, UnitPrice, Quantity, Discount FROM [Order Details]";

private const string SELECT_CUSTOMERS = "C.CustomerID, CompanyName, ContactName, ContactTitle, [Address], City, Region, PostalCode, Country, Phone, Fax, Since, Points, CardNumber, CAST((CASE M.CustomerID when NULL then 0 else 1 END) AS bit) AS IsMember FROM Customers AS C LEFT OUTER JOIN Members AS M ON C.CustomerID = M.CustomerID";

 

public Order GetOrder(int orderID)

{

    return ReadEntity<Order>(string.Format("SELECT TOP 1 {0} WHERE OrderID = @OrderID; \n SELECT {1} WHERE OrderID = @OrderID;", SELECT_ORDERS, SELECT_ORDERDETAILS), new[] { new SqlParameter("OrderID", orderID) }, reader => GetOrder(reader), (o, reader, connection) =>

    {

        o.OrderDetails = new List<OrderDetail>();

        reader.NextResult();

        while (reader.Read())

        {

            var orderDetail = GetOrderDetail(reader);

            orderDetail.Order = o;

            o.OrderDetails.Add(orderDetail);

        }

        if (o.CustomerID != null)

            o.Customer = ReadEntity<Customer>(string.Format("SELECT TOP 1 {0} WHERE C.CustomerID = @CustomerID", SELECT_CUSTOMERS), new[] { new SqlParameter("CustomerID", o.CustomerID) }, subReader => GetCustomer(subReader), connection);

        var pq = o.OrderDetails.Select(od => od.ProductID.ToString());

        if (pq.Any())

        {

            var products = ReadEntities<Product>(string.Format("SELECT {0} WHERE ProductID IN ({1})", SELECT_PRODUCTS, pq.Skip(1).Any() ? pq.Aggregate((p1, p2) => p1.Contains(string.Format(" {0} ", p2)) ? p1 : string.Concat(" ", p1, " , ", p2)) : pq.First()), subReader => GetProduct(subReader), connection);

            foreach (var p in products)

                p.OrderDetails = o.OrderDetails.Where(od => od.ProductID == p.ProductID).Select(od => { od.Product = p; return od; }).ToList();

        }

    });

}

 

private T ReadEntity<T>(string commandText, SqlParameter[] parameters, Func<SqlDataReader, T> getEntityFromReader, Action<T, SqlDataReader, SqlConnection> moreAction = null) where T : class

{

    SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["NorthwindEntities"].ToString());

    connection.Open();

    try

    {

        return ReadEntity(commandText, parameters, getEntityFromReader, connection, moreAction);

    }

    finally

    {

        connection.Close();

    }

}

 

private T ReadEntity<T>(string commandText, SqlParameter[] parameters, Func<SqlDataReader, T> getEntityFromReader, SqlConnection connection, Action<T, SqlDataReader, SqlConnection> moreAction = null) where T : class

{

    SqlCommand command = connection.CreateCommand();

    command.CommandText = commandText;

    command.Parameters.AddRange(parameters);

    SqlDataReader reader = command.ExecuteReader();

    try

    {

        if (reader.Read())

        {

            var value = getEntityFromReader(reader);

            if (moreAction != null)

                moreAction(value, reader, connection);

            return value;

        }

        return null;

    }

    finally

    {

        reader.Close();

    }

}

 

private List<T> ReadEntities<T>(string commandText, Func<SqlDataReader, T> getEntityFromReader, SqlConnection connection) where T : class

{

    var value = new List<T>();

    SqlCommand command = connection.CreateCommand();

    command.CommandText = commandText;

    SqlDataReader reader = command.ExecuteReader();

    try

    {

        while (reader.Read())

            value.Add(getEntityFromReader(reader));

        return value;

    }

    finally

    {

        reader.Close();

    }

}

 

private Order GetOrder(SqlDataReader reader)

{

    return new Order { OrderID = reader.GetInt32(0), CustomerID = reader.IsDBNull(1) ? null : reader.GetString(1), EmployeeID = reader.IsDBNull(2) ? null : (int?)reader.GetInt32(2), OrderDate = reader.IsDBNull(3) ? null : (DateTime?)reader.GetDateTime(3), RequiredDate = reader.IsDBNull(4) ? null : (DateTime?)reader.GetDateTime(4), ShippedDate = reader.IsDBNull(5) ? null : (DateTime?)reader.GetDateTime(5), ShipVia = reader.IsDBNull(6) ? null : (int?)reader.GetInt32(6), Freight = reader.IsDBNull(7) ? null : (decimal?)reader.GetDecimal(7), ShipName = reader.IsDBNull(8) ? null : reader.GetString(8), ShipAddress = reader.IsDBNull(9) ? null : reader.GetString(9), ShipCity = reader.IsDBNull(10) ? null : reader.GetString(10), ShipRegion = reader.IsDBNull(11) ? null : reader.GetString(11), ShipPostalCode = reader.IsDBNull(12) ? null : reader.GetString(12), ShipCountry = reader.IsDBNull(13) ? null : reader.GetString(13) };

}

 

private OrderDetail GetOrderDetail(SqlDataReader reader)

{

    return new OrderDetail { OrderID = reader.GetInt32(0), ProductID = reader.GetInt32(1), UnitPrice = reader.GetDecimal(2), Quantity = reader.GetInt16(3), Discount = reader.GetFloat(4) };

}

 

private Customer GetCustomer(SqlDataReader reader)

{

    if (reader.GetBoolean(14))

        return GetMember(reader);

    return GetCustomer<Customer>(reader);

}

 

private T GetCustomer<T>(SqlDataReader reader) where T : Customer, new()

{

    return new T { CustomerID = reader.GetString(0), CompanyName = reader.GetString(1), ContactName = reader.IsDBNull(2) ? null : reader.GetString(2), ContactTitle = reader.IsDBNull(3) ? null : reader.GetString(3), Address = reader.IsDBNull(4) ? null : reader.GetString(4), City = reader.IsDBNull(5) ? null : reader.GetString(5), Region = reader.IsDBNull(6) ? null : reader.GetString(6), PostalCode = reader.IsDBNull(7) ? null : reader.GetString(7), Country = reader.IsDBNull(8) ? null : reader.GetString(8), Phone = reader.IsDBNull(9) ? null : reader.GetString(9), Fax = reader.IsDBNull(10) ? null : reader.GetString(10) };

}

 

private Member GetMember(SqlDataReader reader)

{

    var member = GetCustomer<Member>(reader);

    member.Since = reader.IsDBNull(11) ? null : (DateTime?)reader.GetDateTime(11);

    member.Points = reader.IsDBNull(12) ? null : (int?)reader.GetInt32(12);

    member.CardNumber = reader.IsDBNull(13) ? null : reader.GetString(13);

    return member;

}

 

private Product GetProduct(SqlDataReader dataReader)

{

    return new Product { ProductID = dataReader.GetInt32(0), ProductName = dataReader.GetString(1), SupplierID = dataReader.IsDBNull(2) ? null : (int?)dataReader.GetInt32(2), CategoryID = dataReader.IsDBNull(3) ? null : (int?)dataReader.GetInt32(3), QuantityPerUnit = dataReader.IsDBNull(4) ? null : dataReader.GetString(4), UnitPrice = dataReader.IsDBNull(5) ? null : (decimal?)dataReader.GetDecimal(5), UnitsInStock = dataReader.IsDBNull(6) ? null : (short?)dataReader.GetInt16(6), UnitsOnOrder = dataReader.IsDBNull(7) ? null : (short?)dataReader.GetInt16(7), ReorderLevel = dataReader.IsDBNull(8) ? null : (short?)dataReader.GetInt16(8), Discontinued = dataReader.GetBoolean(9) };

}

Tout simplement hallucinant !

Avec EF, cela donne tout simplement ceci :

//Generated file

partial class NorthwindService

{

    private static Func<NorthwindEntities, System.Int32, Order> GetOrderCQ = CompiledQuery.Compile<NorthwindEntities, System.Int32, Order>((context, OrderID) => context.Orders.OfType<Order>().FirstOrDefault(e => e.OrderID == OrderID));

 

    private static Func<NorthwindEntities, System.Int32, Order> GetOrderWithIncludeCQ { get; set; }

 

    public Order GetOrder(System.Int32 OrderID)

    {

        using (var context = new NorthwindEntities())

        {

            if (GetOrderWithIncludeCQ != null)

                return GetOrderWithIncludeCQ(context, OrderID);

            return GetOrderCQ(context, OrderID);

        }

    }

}

 

//My partial part

partial class NorthwindService

{

    static NorthwindService()

    {

        GetOrderWithIncludeCQ = (context, orderID) => context.Orders.Include("OrderDetails.Product").Include("Customer").FirstOrDefault(o => o.OrderID == orderID);

    }

}

Je ne sais pas ce que vous en pensez mais perso je trouve ça un peu plus facile à lire… et un peu plus rapide à écrire

D’autre part, comme je l’ai expliqué plus haut, finalement je n’ai écrit qu’une seule ligne de code :

GetOrderWithIncludeCQ = (context, orderID) => context.Orders.Include("OrderDetails.Product").Include("Customer").FirstOrDefault(o => o.OrderID == orderID);

Vive la productivité !

Cela nous donne donc le temps de dev en fonction du développement suivant :

Nb entités / temps de dev

1

2

3

4

5

6

7

8

9

10

11

12

13

14

100

500

1000

EF

3

3

4

4

4

4

5

5

5

5

6

6

6

6

28

128

253

ADO .NET 2.0

35

65

95

125

155

185

215

245

275

305

335

365

395

425

3005

15005

30005

C’est à dire que pour 1000 entités, avec ADO 2.0, cela prend plus de 66 jours / homme (à raison de 7.5 h par jour) contre… 4 heures pour Entity Framework. Essayons d’être honnête dans les estimations, dans le cas de 1000 entités, il est souhaitable de de découper nos entités en plusieurs modèles. On va donc grossir le trait à 2 jours / homme pour EF et arrondir le développement avec ADO 2.0 à 2.5 mois. Impressionnant non ? CQFD

Si on rajoute à ça le fait que le code est plus lisible avec EF, je ne vois plus beaucoup de raison de vouloir continuer à faire de l’ADO .NET 2.0.

Il y a tout de même un point très important à préciser : pensez à vous former avant de réellement développer avec EF (vous pouvez d’ailleurs me contacter pour cela (matthieu.mezil at live.fr)). Ceci est très important car même si ça a l’air simple d’utilisation, il y a des concepts à maîtriser pour :

  • ne pas perdre du temps lors du développement
  • obtenir les résultats attendus (notamment au niveau de la gestion des entités non persistées)
  • ne pas dégrader les perfs

Vous trouverez le tt que j’utilise ici.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié mardi 20 octobre 2009 20:40 par Matthieu MEZIL

Classé sous : , , , ,

Commentaires

# re: EF : la voie de la productivité @ mardi 20 octobre 2009 22:11

Je dois être trop vieux : comprends rien !!!

richardc

# re: EF : la voie de la productivité @ mercredi 21 octobre 2009 01:51

J'ai pas compris ce que le T4 a à voir dans tout ça ?

tja

# re: EF : la voie de la productivité @ mercredi 21 octobre 2009 09:54

Hello

Dans l'ensemble je suis d'accord, mais il ne faut oublier que Visual Studio permettait en "clic clic clic" de générer assez rapidement DataSet, DataAdapter, etc., le tout typé et facilement maintenable.

Alors c'est vrai que EF + TT donne qqchose de mieux, mais avant ce n'était déja pas si mal (surtout comparé à "l'encore avant").

SteveB

# re: EF : la voie de la productivité @ mercredi 21 octobre 2009 13:47

et quand est il du support d'oracle avec entity framework ?

minsou

# re: EF : la voie de la productivité @ mercredi 21 octobre 2009 16:36

@Richard : comment ça ?

@tja : T4 me permet de générer mon service WCF à partir de mon edmx

@SteveB : Plusieurs défauts au dataset :

  1. pas de propriété Nullable : pénible de tester si la propriété n'est pas null

  2. si tu veux charger un Customer avec ses Orders, tu dois ajouter ta propre requête avec un Where dans le DataAdapter des Orders (pas très dur j'en convient). De même pour récupérer en plus les products de tes orderdetails de tes orders, tu vas devoir prévoir encore une nouvelle requête avec IN. Ce qui est embêtant dans ce cas, c'est les perfs : tu vas avoir n requêtes exécutées une par une (une par table) en base. Avec EF, tu n'en as qu'une seule.

  3. Facilement maintenable oui et non : si tu as du mapping 1..1 avec ta base oui mais je ne préconise surtout pas cette approche. Je préfère concevoir mes entités en objet et non pas en relationnelle. Il te faut donc une surcouche d'entités qui va te prendre du temps à écrire et qui va être fortement dépendante du Dataset =&gt; (modif du DataSet =&gt; modif des entités). En plus, sauf erreur de ma part, les classes générées ne sont pas des DataContracts et il n'est pas conseillé de rajouter des attributs DataMember sur les propriétés (code généré) ce qui nous ramène à encapsuler les tables du DataSets dans de vraies entités ou à écrire toi-même un générateur de classes à partir du xml du DataSet.

Bref le DataSet c'est gentil mais pas terrible en comparaison avec EF.

@minsou : MS ne bosse pas dessus. Je crois qu'Oracle oui mais sinon, il y a déjà au moins deux provider EF pour Oracle développés par des éditeurs tiers et... payant.

Matthieu MEZIL

# re: EF : la voie de la productivité @ mercredi 21 octobre 2009 18:35

Concernant le DataSet ce n'est même pas comparable avec EF.

Développer avec les DataSet c'est ce concentrer sur les données sans se vraiment préoccuper du modèle métier. En gros pour mois c'est pas très compatible avec la POO sans parler de TDD etc,

tja

# re: EF : la voie de la productivité @ mercredi 21 octobre 2009 23:53

DataSets "pas très compatible avec la POO sans parler de TDD". 100 % d'accord avec toi

Matthieu MEZIL

# re: EF : la voie de la productivité @ jeudi 22 octobre 2009 11:40

Matthieu,

Tu aurais un livre de référence sur EF qui traite également les nouveautés de EF 4?

Etant l'utilisateur de NHibernate depuis des années je voudrais quand même essayer le nouveau EF qui entre autre supporte les POCO il me semble mais je ne voudrais pas perdre trop de temps en cherchant sur internet :)

tja

# re: EF : la voie de la productivité @ vendredi 23 octobre 2009 00:13

Un bouquin : celui de Julia Lerman. Qui traite de EF4, la v2 qu'elle est en train d'écrire. A ma connaissance il n'y a pas encore de bouquin publié sur EF4.

Matthieu MEZIL

# re: EF : la voie de la productivité @ vendredi 23 octobre 2009 10:30

I'll check it :)

Merci

tja

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01