2009-05-11 6 views
10

Abbiamo un sacco di codice strato di dati che segue questo schema molto generale:DataReader Ritorno da DataLayer in Uso dichiarazione

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 lamentela 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?

+0

Ma non sta caricando tutti i record in memoria in generale meglio di mantenere una connessione aperta al database con un DataReader? – codeulike

+0

Dipende. Per un'app winforms, sì. Per un'app Web in cui la memoria è scarsa e le query devono essere completate rapidamente, probabilmente no. –

+0

Puoi specificare più da vicino quale tipo di "vantaggio reale di DataReader" vorresti avere? –

risposta

13

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. Che mi portano a questo:

public IEnumerable<IDataRecord> 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; 
      } 
     } 
    } 
} 

questo funzionerà ancora meglio una volta ci spostiamo a 3.5 e in grado di iniziare a utilizzare altri operatori LINQ sui risultati, e mi piace perché ci mette fino a cominciare a pensare in termini di una "pipeline" tra ogni livello per query che restituiscono molti risultati.

La nota negativa è che sarà imbarazzante per articoli contenenti più di un set di risultati, ma che è estremamente raro.

Aggiornamento
Da quando ho iniziato a giocare con questo modello nel 2009, ho imparato che è meglio se mi rendono anche un generico IEnumerable<T> tipo di ritorno e aggiungere un parametro Func<IDataRecord, T> per convertire lo stato DataReader per oggetti di business in il cappio. In caso contrario, potrebbero verificarsi problemi con l'iterazione lenta, in modo tale da visualizzare l'ultimo oggetto nella query ogni volta.

+0

Sto usando un'implementazione simile per .NET 3.5. Funziona bene, ma per qualche ragione sento un abuso del pattern iteratore, ma molto soggettivo. E 'davvero complicato quando si vuole avvolgere qualche gestione delle eccezioni che impedisce a questo blocco di uscire perché il cast fallisce per qualche motivo. ;-) –

+1

@RunningMonkey: dal momento che hai eseguito questo pattern dal vivo, hai visto il cast (IDataRecord) fallire spesso? Quanto dovrei essere preoccupato? –

+0

Forse non molto, perché la mia implementazione è più simile a http://stackoverflow.com/questions/47521/using-yield-to-iterate-over-a-datareader-might-not-close-the-connection. Lì mi converto in tipi base con Convert.ChangeType, prima di fare un ritorno di rendimento. Questo ha quasi pianto per i problemi. :-) –

7

quello che vuoi è un modello supportato, dovrete usare

cmd.ExecuteReader(CommandBehavior.CloseConnection); 

e rimuovere sia using() 's formano il metodo GetSomeData(). La sicurezza eccezionale deve essere fornita dal chiamante, in modo da garantire una chiusura sul lettore.

0

sono mai stato un grande fan di avere lo strato di dati restituisce un oggetto dati generico, in quanto che praticamente si scioglie il punto di aver codice separato in un proprio livello (come si può passare fuori strati di dati se l'isn di interfaccia definito?).

penso che la cosa migliore è per tutte le funzioni di questo tipo per restituire un elenco di oggetti personalizzati creato da voi e nei vostri dati più tardi, si chiama il tuo procedura/query in un DataReader e iterare che la creazione della lista.

Ciò renderà più semplice gestire in generale (nonostante il tempo iniziale per la creazione delle classi personalizzate), rende più semplice gestire la connessione (poiché non si restituiranno oggetti associati ad esso), e dovrebbe sii 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 corretta).

+1

Abbiamo un'architettura a 4 livelli. Il livello dati restituisce oggetti dati _generic_ (DataTable, DataRow, DataReader) a un livello di traduzione che li converte in oggetti business fortemente tipizzati. Riduce al minimo il danno. –

+0

Vedo. Suppongo che l'unico svantaggio sarebbe fare il loop in entrambi gli strati per ottenere la lista creata, ma penserei che l'impatto sarebbe minimo. – John

+1

La cosa grandiosa di IEnumerable è che si esegue ancora una volta il ciclo dei dati. Quindi sì, l'impatto dovrebbe essere inesistente. –

3

In tempi come questi trovo che lambda possono essere di grande utilità.Considerate questo, al posto dello strato di dati dandoci i dati, diamo lo strato di dati il ​​nostro metodo di elaborazione dei dati:

public void GetSomeData(string filter, Action<IDataReader> processor) 
{ 
    ... 

    using (IDataReader reader = cmd.ExecuteReader()) 
    { 
     processor(reader); 
    } 
} 

Poi lo strato di business lo chiamano:

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

.Net 2.0 per ora, ma terrò questo a mente per il prossimo aggiornamento. –

+0

In realtà, non penso che funzionerà perché voglio che questi siano passati la catena verso il livello di presentazione. Potrei ancora farlo ripetendo questo schema all'interno del lambda, ma questo sembra brutto. –

+0

Sono d'accordo. Poi, di nuovo, tendo a voler lavorare e chiudere il lettore di dati il ​​più rapidamente possibile, e piuttosto a costruire i dati richiesti al livello dati, lasciandolo ai livelli precedenti per dettare come dovrebbero apparire quei dati. Con il processore Action, il livello aziendale impone di dettare il formato mantenendo il loop del lettore il più stretto possibile (assumendo che il livello aziendale non comporti un dispendio di tempo nel metodo del processore, cioè). –

2

La chiave è yield parola chiave.

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

public IEnumerable<S> Get<S>(string query, Action<IDbCommand> parameterizer, 
          Func<IDataRecord, S> 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); 
} 

Così mi chiamano:

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

} 

Ciò è pienamente generico, adatto a qualsiasi modello conforme alle interfacce di ado.net. The connection and reader objects are disposed after the collection is enumerated. In ogni caso il riempimento di un DataTable utilizzando IDataAdapter 's metodo Fillcan be faster than DataTable.Load

+0

Sono un po 'confuso dalla nota che "l'oggetto di connessione e il lettore sono disposti solo dopo che la raccolta viene enumerata una volta." Tutto quello che sto leggendo, inclusa la domanda SO collegata dalla tua frase, suggerisce che anche se la raccolta non è completamente enumerata (ad esempio uscendo da un 'foreach' che la sta usando), sarà eliminata. Stai dicendo che non lo è, o se esce dall'iterazione perché la collezione viene "enumerata una volta?" Grazie per aver arricchito la versione di Func , a proposito. – bubbleking

+1

@bubbleking Ho fatto una dichiarazione fuorviante lì. Stavo cercando di comunicare che gli oggetti di connessione e di lettura saranno disposti dopo averlo enumerato nell'elenco, se non del tutto necessario enumerare, indipendentemente dal fatto che si interrompa il ciclo o meno. Se non si enumera, non vi è alcuna questione di oggetto di connessione mai usato. Modificherò la risposta. Grazie per la segnalazione. – nawfal