Restituisce DataReader da DataLayer in Uso dell’istruzione

Abbiamo un sacco di codice del livello dati che segue questo modello molto generale:

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

Penso che possiamo fare un po ‘meglio. La mia lamencanvas principale in questo momento è che obbliga tutti i record a essere caricati in memoria, anche per i set di grandi dimensioni. Mi piacerebbe essere in grado di sfruttare l’abilità di un DataReader di tenere un solo record in ram alla volta, ma se restituisco direttamente il DataReader, la connessione viene interrotta quando si esce dal blocco using.

Come posso migliorare questo per consentire il ritorno di una riga alla volta?

Ancora una volta, l’atto di comporre i miei pensieri per la domanda rivela la risposta. Nello specifico, l’ultima frase in cui ho scritto “una riga alla volta”. Mi sono reso conto che non mi interessa che sia un datareader, finché posso enumerarlo riga per riga. Questo mi ha portato a questo:

 public IEnumerable GetSomeData(string filter) { string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter"; using (SqlConnection cn = new SqlConnection(GetConnectionString())) 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; } } } } 

Funzionerà ancora meglio una volta passati a 3.5 e possiamo iniziare ad usare altri operatori linq sui risultati, e mi piace perché ci imposta per iniziare a pensare in termini di “pipeline” tra ogni livello per le query che restituiscono un sacco di risultati.

Il lato negativo è che sarà difficile per i lettori in possesso di più di un set di risultati, ma questo è estremamente raro.

Aggiornare
Da quando ho iniziato a giocare con questo modello nel 2009, ho imparato che è meglio se anche io lo Func un generico IEnumerable restituisce il tipo e aggiungo un parametro Func per convertire lo stato DataReader in oggetti business nel ciclo continuo. In caso contrario, potrebbero verificarsi problemi con l’iterazione lenta, in modo tale da visualizzare l’ultimo object nella query ogni volta.

Quello che vuoi è un modello supportato, dovrai usarlo

 cmd.ExecuteReader(CommandBehavior.CloseConnection); 

e rimuovi entrambi using() il tuo metodo GetSomeData (). La sicurezza eccezionale deve essere fornita dal chiamante, in modo da garantire una chiusura sul lettore.

In tempi come questi, trovo che lambda può essere di grande utilità. Considera questo, anziché il livello dati che ci fornisce i dati, diamo al livello dati il ​​nostro metodo di elaborazione dei dati:

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

Quindi il livello aziendale lo chiamerebbe:

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

La chiave è la parola chiave yield .

Simile alla risposta originale di Joel, poco più ritmata:

 public IEnumerable Get(string 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.ConnectionString = _connectionString; cmd.Connection.Open(); using (var r = cmd.ExecuteReader()) while (r.Read()) yield return selector(r); } } } 

E ho questo metodo di estensione:

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

Quindi chiamo:

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

Questo è completamente generico, si adatta a qualsiasi modello che rispetti le interfacce di ado.net. Gli oggetti di connessione e di lettura sono disposti dopo che la raccolta è stata elencata. Comunque compilare un DataTable usando il metodo Fill IDataAdapter può essere più veloce di DataTable.Load

Non sono mai stato un grande fan del fatto che il livello dati restituisse un object dati generico, dal momento che praticamente dissolve l’intero punto di avere il codice separato nel proprio livello (come è ansible cambiare i livelli di dati se l’interfaccia non è definita? ).

Penso che la soluzione migliore per tutte le funzioni di questo tipo sia quella di restituire un elenco di oggetti personalizzati che crei tu stesso, e nei tuoi dati in seguito, chiami la procedura / query in un datareader e iterate attraverso quella che crea l’elenco.

Ciò renderà più semplice trattare in generale (nonostante il tempo iniziale per creare le classi personalizzate), rende più facile gestire la connessione (dato che non restituirai alcun object associato ad esso), e dovrebbe essere più veloce. L’unico svantaggio è che tutto verrà caricato nella memoria come hai detto tu, ma non penserei che questo sarebbe motivo di preoccupazione (se lo fosse, penserei che la query dovrebbe essere regolata).