Por John Papa
| Este artigo discute: | Este artigo usa as seguintes tecnologias: |
| | -
ADO.NET -
Visual Studio .NET 2005 |
O ADO.NET 2.0 traz algumas melhorias interessantes para as classes básicas do ADO.NET 1.x e introduz uma variedade de classes novas, todas prometendo melhorar o desempenho, a flexibilidade e a eficiência. Houve algumas mudanças importantes mesmo ao longo do ciclo de vida das versões pre-beta e beta do ADO.NET 2.0, assim como as melhorias para o novo processo de batch updating (atualizações em lote). Com a aproximação do lançamento final do ADO.NET 2.0 e o conjunto de novos recursos ficando mais estável, está na hora de dar uma olhada no que vem por aí.
Este mês começarei explorando as melhorias nas classes DataSet e DataTable, o que elas significam para você e quando devem ser utilizadas. Às vezes, no ADO.NET 1.x, especialmente ao trabalhar com grandes conjuntos de dados, você pode ter problemas relacionados ao desempenho. Discutirei como alguns destes problemas de desempenho no ADO.NET 2.0 foram resolvidos através de mudanças no engine de indexação, falarei sobre as características que foram acrescentadas à classe DataTable e as opções de carga pelo novo método Load e os novos métodos que permitem modificar o estado de um registro (RowState). Na próxima edição desta coluna, discutirei outras melhorias, como a habilidade para executar batch updating, a compressão de DataSets para transporte usando serialização binária, e mais.
Melhorias no DataTable
No ADO.NET 1.x o DataSet obteve todas as "glórias" deixando o DataTable nas "sombras". Sem falar que o DataTable não era uma classe muito útil por si própria (quando usada sem um DataSet). O DataTable funciona como um conjunto de linhas e colunas e poder ser considerado a principal classe do ADO.NET para armazenar dados desconectados. No entanto, o DataSet é mais interessante porque pode conter uma coleção de objetos DataRelation e DataTable.
Apesar de útil, o DataTable tem algumas limitações no ADO.NET 1.x que o DataSet não tem. Por exemplo, o DataSet expõe um método Merge que pode "fundir" dois objetos DataTable dentro de um DataSet, mas o próprio DataTable não expõe um método Merge. Assim, se você estivesse usando um DataTable (não contido dentro de um DataSet) e você quisesse fundi-lo com outro objeto DataTable, teria que criar antes um objeto DataSet, colocar o primeiro DataTable nele e invocar o método DataSet.Merge, como fiz na Listagem 1.
Listagem 1. Fusão de dois objetos DataTable no ADO.NET 1.x
string sqlAllCustomers = "SELECT * FROM Customers";
string cnStr =
@"Data Source=.;Initial Catalog=northwind;Integrated Security=True";
using (SqlConnection cn = new SqlConnection(cnStr))
{
cn.Open();
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCust1 = new DataTable("Customers");
adpt.Fill(dtCust1);
dtCust1.PrimaryKey = new DataColumn[]{dtCust1.Columns["CustomerID"]};
DataTable dtCust2 = dtCust1.Clone();
DataRow row1 = dtCust2.NewRow();
row1["CustomerID"] = "ALFKI";
row1["CompanyName"] = "Some Company";
dtCust2.Rows.Add(row1);
DataRow row2 = dtCust2.NewRow();
row2["CustomerID"] = "FOO";
row2["CompanyName"] = "Some Other Company";
dtCust2.Rows.Add(row2);
DataSet ds = new DataSet("MySillyDataSet");
ds.Tables.Add(dtCust1);
ds.Merge(dtCust2);
dgTest.DataSource = dtCust1;
}
Não é difícil, mas é chato. No ADO.NET 2.0, o objeto DataTable agora tem um método Merge, assim você pode fundir dois objetos DataTable (sem ter que criar um DataSet), como a seguir:
dtCust1.Merge(dtCust2);
Outro inconveniente do ADO.NET 1.x é que você não pode executar as operações básicas para suporte à XML em um DataTable, sem associá-lo antes a um DataSet. Por exemplo, se você quiser salvar um DataTable para XML, precisa carregar o DataTable em um DataSet e usar o método WriteXml do DataSet. Porém, No ADO.NET 2.0, o DataTable tem agora um método WriteXml, assim o problema fica resolvido. Além do método WriteXml, o DataTable no ADO.NET 2.0 expõe também o ReadXml, ReadXmlSchema e WriteXmlSchema.
O DataSet também tem alguns novos métodos e propriedades. DataSet e DataTable expõem agora a propriedade RemotingFormat, assim como também os métodos Load e CreateDataReader. A propriedade RemotingFormat é usada para indicar o formato de serialização (binário ou XML) do DataTable ou DataSet. O método Load pode ser usado para carregar dados em um DataTable ou DataSet, de diversas formas, como veremos brevemente a seguir.
DataTableReader
O método CreateDataReader do DataTable (chamado GetDataReader em versões beta anteriores) cria uma instância da classe DataTableReader do ADO.NET 2.0. Um DataTableReader criado usando DataTable.CreateDataReader conterá as mesmas linhas e colunas tal como no DataTable. Quando um DataTableReader é criado a partir de um DataSet ou pelo método CreateDataReader de um DataTable, o DataTableReader conterá todas as linhas do respectivo objeto, com a exceção das linhas apagadas.
O DataTableReader é um objeto mais leve que o DataTable e, diferente do DataReader (SqlDataReader), o DataTableReader é desconectado. Esta é uma grande característica porque você tem um objeto leve que pode iterar rapidamente (como no DataReader) e está desconectado de qualquer fonte de dados (diferente do DataReader). Um DataTableReader é usado para se fazer uma iteração sobre as linhas da tabela associada, semelhante ao que fazíamos usando um foreach na coleção Rows. Porém, ao contrário de enumerar as linhas de uma tabela (o que causaria uma exceção quando uma linha fosse adicionada ou apagada da coleção durante a varredura), um DataTableReader é flexível em relação às mudanças feitas na tabela e se ajusta de acordo com as modificações.
O exemplo a seguir mostra como você pode criar um DataTableReader e pode ligá-lo a um DataGridView:
using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers);
DataTableReader dtRdr = ds.CreateDataReader();
dgvCustomers.DataSource = dtRdr;
}
O DataTableReader só pode ser percorrido para frente, igual ao DataReader. Novamente, como no DataReader, você se move para a primeira linha usando o método Read do DataTableReader. Se o DataTableReader foi criado a partir de um DataSet que contém múltiplos DataTables, o DataTableReader conterá múltiplos resultsets (um por DataTable). Cada resultset subseqüente pode ser acessado usando-se o método NextResult (novamente, semelhante ao DataReader).
A Listagem 2 mostra como criar um DataTableReader de um DataSet que contém dois objetos DataTable. Considerando que o DataSet tem dois DataTables, o DataTableReader conterá dois resultsets. Você pode percorrer ambos os resultsets no DataTableReader usando os métodos Read e NextResult, como mostrado na Listagem 2. Lembre-se que o DataTableReader só se move para frente. Assim se você quiser acessar o DataTableReader duas vezes, para percorrê-lo novamente, você terá que recarregá-lo após a leitura completa dos registros.
Listagem 2. Percorrendo um DataTableReader
using (SqlConnection cn = new SqlConnection(cnStr))
{
// Cria o Command e o Adapter
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
// Cria e preenche o DataTable
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers);
DataSet ds = new DataSet();
ds.Tables.Add(dtCustomers);
adpt.SelectCommand = new SqlCommand("SELECT * FROM Orders", cn);
adpt.Fill(ds, "Orders");
// Cria o DataTableReader (está desconectado)
using(DataTableReader dtRdr = ds.CreateDataReader())
{
do
{
Console.WriteLine("******************************");
while (dtRdr.Read())
{
Console.WriteLine(dtRdr.GetValue(0).ToString());
}
}
while (dtRdr.NextResult());
}
}
Na Listagem 2 o DataSet cria um DataTableReader usando o método CreateDataReader. A ordem na qual os resultsets do DataTable são adicionados ao DataTableReader é a mesma na qual eles aparecem no DataSet. Se você quiser especificar a ordem dos resultsets, use uma versão sobrecarregada do método CreateDataReader.
Carregando dados
Um DataTableReader também pode ser usado para preencher um DataTable ou um DataSet. Na realidade, usando o novo método Load de um DataSet ou DataTable, você pode passar um DataTableReader ou qualquer classe de leitura que implemente a interface IDataReader. O seguinte exemplo assume que existe um DataTable chamado dt1 que contém um schema e algumas linhas, cria um DataTableReader a partir dele e então carrega um segundo DataTable (chamado dt2) com os mesmos dados:
DataTableReader dtRdr = dt1.CreateDataReader();
DataTable dt2 = new DataTable();
dt2.Load(dtRdr);
No ADO.NET 1.x você poderia preencher um DataSet ou DataTable usando o método Fill do DataAdapter. Alternativamente, você poderia preencher um DataSet XML usando o método ReadXml do DataSet. A introdução do método Load no ADO.NET 2.0 torna possível carregar um DataSet ou um DataTable de uma classe que implementa IDataReader (como um DataTableReader ou o SqlDataReader). Usando o método Load para carregar várias linhas você pode desativar a verificação/manutenção de índices, verificação de constraints e outras notificações, simplesmente invocando antes o método BeginLoadData, e então ativá-los novamente invocando o método EndLoadData. Esses métodos que estão disponíveis no ADO.NET 1.x podem fazer mais rapidamente a carga de dados, pois o ADO.NET não tem que parar depois de cada linha e recriar os índices, verificar Constraints etc.
A enumeração LoadOption
Os métodos Load e Fill têm uma versão sobrecarregada que aceita um dos valores da enumeração LoadOption. Essas configurações são bastante poderosas e podem ser usadas para indicar como as linhas devem substituir linhas existentes durante uma operação Fill ou Load em um DataSet ou DataTable. Esse processo assume que uma chave primária foi configurada, pois ela é usada para determinar como substituir ou juntar as linhas. Os possíveis valores da enumeração LoadOption ajudam a determinar se o valor atual e/ou valor original será sobrescrito com os valores das novas linhas. A Tabela 1 mostra as três opções e uma breve descrição de cada uma.
Tabela 1. Opções da enumeração LoadOption
| Opção | Descrição |
| PreserveChanges | Esta é a opção padrão. Mantém os valores atuais. Sobrescreve os valores originais com as novas linhas. |
| Upsert | Sobrescreve os valores atuais com as novas linhas. Mantém os valores originais. |
| OverwriteChanges | Sobrescreve os valores originais com as novas linhas. Sobrescreve os valores originais com as novas linhas. |
Cada uma dessas opções tem sua função, dependendo da aplicação e circunstância. A opção OverwriteChanges trabalha bem se você tiver um DataTable que contém dados, mas você quer obter valores modificados no banco de dados. O uso dessa opção sobrescreverá tanto os valores originais e atuais no DataTable com os valores obtidos do banco de dados. O importante aqui é que, ao usar OverwriteChanges, qualquer dado que foi modificado (versão original ou versão atual) no primeiro DataTable será sobrescrito, enquanto que novas linhas serão adicionadas.
As colunas do DataSet armazenam os valores originais e atuais. PreserveChanges manterá o valor atual intacto sobrescrevendo o valor original. Upsert faz o oposto disso mantendo o valor original intacto e sobrescrevendo o valor atual.
Aqui vai um exemplo de quando PreserveChanges poderia ser útil. Suponha que uma usuária chamada "Peggy" abriu uma tela e carregou um DataGrid com clientes de um DataSet. Peggy modifica a cidade do cliente com CustomerID "ALFKI" de "Berlim" para "New York", mas não clica no botão Save. Ela vai tomar uma xícara de café. Enquanto isto, "Katherine" modifica a cidade do mesmo cliente de "Berlim" para "Miami". Você terá um problema de conflito de dados caso Peggy voltar para salvar o registro. Nessa situação, o valor original para o registro de cliente de Peggy era "Berlim" e desde que ela o mudou para "New York" o valor atual no BD é "New York". Enquanto isso, no banco de dados a cidade é agora "Miami". Se você quiser restaurar os valores originais do DataSet de Peggy a partir do que está no banco de dados, você poderia obter os dados do BD em um DataTableReader e então poderia carregá-los no DataSet usando LoadOption.PreserveChanges. A Listagem 3 ilustra como isto funciona.
Listagem 3. Testando o LoadOptions
using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers);
dtCustomers.PrimaryKey = new DataColumn[] {
dtCustomers.Columns["CustomerID"] };
// Dê uma pausa aqui para executar este SQL diretamente no banco de dados:
// UPDATE Customers SET City = 'Miami' WHERE CustomerID = 'ALFKI'
System.Diagnostics.Debugger.Break();
// Muda os valores atuais no DataTable
DataRow row = dtCustomers.Rows.Find("ALFKI");
row["City"] = "Somewhere";
// ORIGINAL city == Berlin
// CURRENT city == New York
DisplayDataRowVersions(row, "Immediately after I change the City" +
" from Berlin to New York", "City");
// Carrega outro DataTable com os dados do cliente
DataTable dtCustomers2 = new DataTable("Customers");
adpt.Fill(dtCustomers2);
DataTableReader dtRdrCustomers = dtCustomers2.CreateDataReader();
LoadOption opt = LoadOption.PreserveChanges;
switch (opt)
{
case LoadOption.OverwriteChanges:
// Sobrescreve os valores ORIGINAL e CURRENT
dtCustomers.Load(dtRdrCustomers, LoadOption.OverwriteChanges);
// ORIGINAL city == Miami
// CURRENT city == Miami
row = dtCustomers.Rows.Find("ALFKI");
ShowVersions (row,
"Immediately after LoadOptions.OverwriteChanges",
"City");
break;
case LoadOption.Upsert:
// Mantém os valores ORIGINAL e sobrescreve valores CURRENT
dtCustomers.Load(dtRdrCustomers, LoadOption.Upsert);
// ORIGINAL city == Berlin
/// CURRENT city == Miami
row = dtCustomers.Rows.Find("ALFKI");
ShowVersions (row,
"Immediately after LoadOptions.Upsert", "City");
break;
case LoadOption.PreserveChanges:
// Sobreescre valores ORIGINAL e mantém os valores CURRENT
dtCustomers.Load(dtRdrCustomers, LoadOption.PreserveChanges);
// ORIGINAL city == Miami
// CURRENT city == New York
row = dtCustomers.Rows.Find("ALFKI");
ShowVersions (row,
"Immediately after LoadOptions.PreserveChanges", "City");
break;
}
}
Tente executar esse código no depurador para verificar como essas opções funcionam. Teste diferentes valores da enumeração LoadOption, mudando a variável opt que aparece imediatamente antes do bloco switch-case. O código mostrado na Listagem 3 invoca o método ShowVersions que simplesmente exibe na janela Output da IDE a versão original e a versão atual de uma determinada coluna.
Mudando o RowState
O estado de uma linha (RowState) é o fator principal que ajuda a determinar quais linhas devem ser atualizadas, inseridas ou apagadas quando o método DataAdapter.Update é invocado. O RowState também é examinado pelo método GetChanges, que também determina quais linhas manter. Quando você modifica valores em um DataSet, o ADO.NET automaticamente configura o RowState do registro afetado, que pode ser Modified, Added, ou Unchanged.
Às vezes seria realmente útil poder configurar o estado de uma linha diretamente. É aqui que os novos métodos SetModified e SetAdded do DataRow facilitam as coisas. Por exemplo, vamos assumir que você está frente a uma situação na qual precisa copiar várias linhas de um banco de dados para outro usando ADO.NET. Usando ADO.NET 2.0 você poderia preencher um DataTable de um banco de dados usando o método Fill do DataAdapter, mudar as configurações do RowState das linhas para Added, e enviá-los até o segundo banco de dados (assumindo que este usa o mesmo schema) para ser adicionado usando um segundo DataAdapter. Você poderia mudar o RowState das linhas de Unchanged para Added invocando o método SetAdded de cada linha.
Para demonstrar como esSes métodos trabalham, incluí outro exemplo, mostrado no código da Listagem 4. EsSe código recupera um conjunto de dados de clientes e configura o RowState de dois registros para Modified e de outro para Added. Então, usa o método GetChanges do DataTable para criar outro DataTable que contém as linhas com RowState Modified, armazenando ainda o total de linhas. A seguir, obtém o número de linhas que foram adicionadas e o exibe usando um MessageBox.
Listagem 4. Mudando o RowState de um registro
DataTable dtCustomers = new DataTable("Customers");
using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
adpt.Fill(dtCustomers);
dtCustomers.PrimaryKey = new DataColumn[] {
dtCustomers.Columns["CustomerID"] };
}
// Muda o RowState de algumas linhas
DataRow row = dtCustomers.Rows.Find("ALFKI");
row.SetModified();
row = dtCustomers.Rows.Find("BOLID");
row.SetModified();
row = dtCustomers.Rows.Find("ANTON");
row.SetAdded();
int modRows = dtCustomers.GetChanges(
DataRowState.Modified).Rows.Count;
int addRows = dtCustomers.GetChanges(DataRowState.Added).Rows.Count;
StringBuilder bldr = new StringBuilder();
bldr.Append(modRows.ToString());
bldr.Append(" row(s) were modified.");
bldr.Append(Environment.NewLine);
bldr.Append(addRows.ToString());
bldr.Append(" row(s) were added");
MessageBox.Show(bldr.ToString());
Os métodos SetAdded e SetModified do DataRow só trabalham com registros que não foram alterados. Outra situação na qual esses métodos são úteis é quando você recebe um DataSet ou um DataTable de um Web Service e as linhas estão todas marcadas como Unchanged. Se você pretender fazer inserções ou atualizações a um banco de dados baseado no DataTable recebido, você poderia configurar o RowState usando esses novos métodos. Caso contrário, se você deixar o RowState como Unchanged, o método Udpate do DataAdpater não enviará as linhas para o UpdateCommand ou o InsertCommand.
Melhorias de desempenho
Uma das melhores novas características do ADO.NET 2.0 não é nem um método novo nem uma classe nova, porém uma focalizada melhoria de desempenho. Uma das maiores reclamações quanto ao DataSet e ao DataTable está relacionada à lentidão para carregar dados, especialmente quando o número de linhas se torna grande (100, 1.000, 10.000 ou mais). A perda de performance é muito grande ao se tentar percorrer um DataTable grande. Para contornar essa limitação, muito esforço foi feito na criação de um engine de indexação mais rápido no ADO.NET 2.0. A recodificação desse engine no ADO.NET resultou em um aumento de desempenho enorme em todas as áreas que incluem carga e fusão de DataTables. A Listagem 5 mostra um exemplo que pode ser rodado em qualquer Visual Studio.NET 2003 (usando ADO.NET 1.1) ou Visual Studio.NET 2005 (usando ADO.NET 2.0). Fiz um teste desse código em ambos os ambientes com números variados de linhas.
Figura 5. Teste de velocidade
DataTable dt = new DataTable("foo");
DataColumn pkCol = new DataColumn("ID", Type.GetType("System.Int32"));
pkCol.AutoIncrement = true;
pkCol.AutoIncrementSeed = 1;
pkCol.AutoIncrementStep = 1;
dt.Columns.Add(pkCol);
dt.PrimaryKey = new DataColumn[] { pkCol };
dt.Columns.Add("SomeNumber", Type.GetType("System.Int32"));
dt.Columns["SomeNumber"].Unique = true;
int limit = 1000000;
int someNumber = limit;
DateTime startTime = DateTime.Now;
for (int i = 1; i <= limit; i++)
{
DataRow row = dt.NewRow();
row["SomeNumber"] = someNumber-;
dt.Rows.Add(row);
}
TimeSpan elapsedTime = DateTime.Now - startTime;
MessageBox.Show(dt.Rows.Count.ToString() +
" rows loaded in " + elapsedTime.TotalSeconds + " seconds.");
O código da Listagem 5 cria um DataTable, acrescenta duas colunas e adiciona 1.000.000 de linhas ao DataTable. Quando o laço é completado, é exibida a quantidade de segundos decorridos, usando um MessageBox. Fiz esse teste usando um número diferente de iterações, no ADO.NET 1.1 e ADO.NET 2.0. Os resultados são mostrados na Tabela 2.
Tabela 2. Resultados de teste de velocidade: ADO.NET 1.1 vs ADO.NET 2.0
| Iterações | ADO.NET 1.1 | ADO.NET 2.0 |
| 10.000 | 0.20 seg. | 0.20 seg. |
| 100.000 | 7.91 seg. | 3.89 seg. |
| 1.000.000 | 1831.01 seg. | 23.78 seg. |
A melhora de velocidade acontece mesmo se não houver nenhuma constraint no DataTable. Por exemplo, quando removi a constraint Unique, pude carregar um milhão de linhas em 1 segundo em ambas as versões do ambiente. Também é importante notar que só carreguei duas colunas e o valor para a coluna SomeNumber era um inteiro seqüencial decrementado. Apesar de que meus resultados podem diferir dos seus, o ponto chave a ser considerado é que o engine de indexação do ADO.NET 2.0 é significativamente mais rápido - tanto mais rápido que agora é totalmente realístico considerar o uso de um DataTable que contenha pelo menos um milhão de linhas.
Vamos aos fatos
No ADO.NET 2.0, o desempenho melhorou em áreas que estavam comprometidas na versão anterior, como carregar um grande número de linhas. Foram adicionadas várias características novas para facilitar o desenvolvimento. A classe DataTable ganhou vários métodos que já existiam na classe DataSet e há até mesmo uma nova classe DataTableReader. Na próxima edição desta coluna, continuarei a discussão do ADO.NET 2.0, examinando como a serialização binária melhora o desempenho, como tirar proveito de batch updating (atualizações em lote), as novas características do DataView, a nova classe SqlConnectionBuilder e muito mais.
John Papa é um Consultor Senior .NET com ASPSOFT e fanático de beisebol que gasta a maioria das noites de verão torcendo para os Yankees com a família e o fiel cachorro Kadi. É autor de vários livros em ADO XML, e SQL Server, e pode ser achado freqüentemente falando em conferências em indústrias como VSLive ou publicando blogs em codebetter.com/blogs/john.papa.