Renvoyer DataReader à partir de DataLayer dans l’instruction Using

Nous avons beaucoup de code de couche de données qui suit ce modèle très général:

public DataTable GetSomeData(ssortingng filter) { ssortingng sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter"; DataTable result = new DataTable(); using (SqlConnection cn = new SqlConnection(GetConnectionSsortingng())) using (SqlCommand cmd = new SqlCommand(sql, cn)) { cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter; result.Load(cmd.ExecuteReader()); } return result; } 

Je pense que nous pouvons faire un peu mieux. Mon principal grief à l’heure actuelle est que cela force le chargement de tous les enregistrements, même pour les grands ensembles. J’aimerais pouvoir tirer parti de la capacité d’un DataReader de ne conserver qu’un enregistrement dans un bélier à la fois, mais si je renvoie directement le DataReader, la connexion est coupée lorsque vous quittez le bloc using.

Comment puis-je améliorer cela pour permettre le retour d’une ligne à la fois?

Encore une fois, le fait de composer mes pensées pour la question révèle la réponse. Plus précisément, la dernière phrase où j’ai écrit “une rangée à la fois”. Je me suis rendu compte que je ne m’inquiète pas vraiment du fait qu’il s’agisse d’un datareader, tant que je peux l’énumérer ligne par ligne. Cela m’amène à ceci:

 public IEnumerable GetSomeData(ssortingng filter) { ssortingng sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter"; using (SqlConnection cn = new SqlConnection(GetConnectionSsortingng())) using (SqlCommand cmd = new SqlCommand(sql, cn)) { cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter; cn.Open(); using (IDataReader rdr = cmd.ExecuteReader()) { while (rdr.Read()) { yield return (IDataRecord)rdr; } } } } 

Cela fonctionnera encore mieux une fois que nous passerons à la version 3.5 et que nous pourrons commencer à utiliser d’autres opérateurs linq pour les résultats, et j’aime bien parce que cela nous permet de commencer à penser en termes de “pipeline” entre chaque couche pour les requêtes renvoyant beaucoup de données. résultats.

L’inconvénient est que cela sera gênant pour les lecteurs qui possèdent plus d’un ensemble de résultats, mais c’est extrêmement rare.

Mettre à jour
Depuis que j’ai commencé à jouer avec ce motif en 2009, j’ai appris qu’il était préférable d’en faire un type de retour IEnumerable générique et d’append un paramètre Func pour convertir l’état DataReader en objects de boucle. Sinon, il peut y avoir des problèmes avec l’itération différée, tels que vous voyez le dernier object de la requête à chaque fois.

Ce que vous voulez, c’est un modèle supporté, vous devrez utiliser

 cmd.ExecuteReader(CommandBehavior.CloseConnection); 

et supprimez tous les deux en using() votre méthode GetSomeData (). L’appelant doit assurer une sécurité exceptionnelle en garantissant une fermeture du lecteur.

Dans des moments comme ceux-là, je trouve que les lambdas peuvent être très utiles. Considérez ceci, au lieu que la couche de données nous donne les données, laissez-nous donner à la couche de données notre méthode de traitement de données:

 public void GetSomeData(ssortingng filter, Action processor) { ... using (IDataReader reader = cmd.ExecuteReader()) { processor(reader); } } 

La couche métier l’appellera alors:

 GetSomeData("my filter", (IDataReader reader) => { while (reader.Read()) { ... } }); 

La clé est le mot-clé de yield .

Semblable à la réponse originale de Joel, un peu plus détaillé:

 public IEnumerable Get(ssortingng query, Action parameterizer, Func selector) { using (var conn = new T()) //your connection object { using (var cmd = conn.CreateCommand()) { if (parameterizer != null) parameterizer(cmd); cmd.CommandText = query; cmd.Connection.ConnectionSsortingng = _connectionSsortingng; cmd.Connection.Open(); using (var r = cmd.ExecuteReader()) while (r.Read()) yield return selector(r); } } } 

Et j’ai cette méthode d’extension:

 public static void Parameterize(this IDbCommand command, ssortingng name, object value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; command.Parameters.Add(parameter); } 

Alors j’appelle:

 foreach(var user in Get(query, cmd => cmd.Parameterize("saved", 1), userSelector)) { } 

Ceci est entièrement générique et convient à tous les modèles conformes aux interfaces ado.net. Les objects connexion et lecteur sont supprimés une fois la collection énumérée. Quoi qu’il en soit, remplir un DataTable aide de la méthode Fill IDataAdapter peut être plus rapide que DataTable.Load

Je n’ai jamais été très enthousiaste à l’idée que la couche de données renvoie un object de données générique, car cela dissout presque le principe de séparation du code dans sa propre couche (comment peut-on changer de couche de données si l’interface n’est pas définie? ).

Je pense que votre meilleur choix est que toutes les fonctions telles que celle-ci renvoient une liste d’objects personnalisés que vous créez vous-même et, ultérieurement, dans vos données, vous appelez votre procédure / requête dans un datareader et effectuez une itération à travers cette création de liste.

Cela facilitera la gestion en général (malgré le temps initial pour créer les classes personnalisées), facilitera la gestion de votre connexion (puisque vous ne renverrez aucun object associé) et devrait être plus rapide. Le seul inconvénient est que tout sera mis en mémoire, comme vous l’avez mentionné, mais je ne penserais pas que ce serait une source de préoccupation (si c’était le cas, je penserais que la requête devrait être ajustée).