none
OutOfMemoryException ao rodar o SaveChanges(). RRS feed

  • Pergunta

  • Olá pessoal,

    Criei uma aplicação winforms, em 3 camadas (cada camada é um projeto diferente) e utilizo o Entity Framework para cuidar da persistência.

    Tudo estava indo relativamente bem, o sistema funciona com a massa de dados que tenho atualmente, mas eu topei com um probleminha.

    Quando eu tentei fazer um merge de dados entre a minha base do sistema antigo para o novo (eu fiz uma remodelagem completa nas tabelas do BD e preciso desses dados para histórico) eu tive um OutOfMemoryException no momento em que chamo o Método SaveChanges().

    Eu criei um projeto novo para fazer o merge e importei minha camada DAL e BLL do sistema, para usar a mesam modelagem e o entity framework.

    O método que deu problema é mais ou menos assim.

    List<DespesaAntiga> despesasAntigas = new Despesas(); despesasAntigas = BusinessFactory.oDespesaAntigaBLL.SelectAll(); foreach(DespesaAntiga despesaAntiga in despesasAntigas){ Despesa despesa = new Despesa(); despesa.Valor = despesaAntiga.Valor; ... BusinessFactory.oDespesaBLL.Insert(despesa); } BusinessFactory.oDespesaBLL.Save(); //Aqui ocorre o OutOfMemoryException MessageBox.Show("Processo Finalizado com Sucesso.");

    A quantidade de dados na tabela de Despesas antiga é bem grande, visto que o sistema roda a seis anos e todo mês temos mais de 10.000 registros de despesas.

    Quando fiz as importações de arquivos mensais de despesas a serem cobradas, que vem de um sistema Cobol no padrão do Governo, com um médoto mais ou menos como o que tem a cima, ele funciona tudo bem, faz todas as operações e no fim salva tudo tranquilamente, mas para um massa maior ele simplesmente usa todos os 4 GB da minha máquina.

    Fiz o método dessa forma, pois queria garantir que os dados só seriam salvos caso todo o processo rode com sucesso.

    O método roda em uma Thread separada da principal, para o usuário conseguir enxergar a barra de status e o progresso do processo.

    Minha dúvida é, como eu poderia modificar meu método de forma que eu possa garantir que os dados só serão salvos se não tiver erro em algum registro e eu não tenha um OutOfMemoryException?


    terça-feira, 22 de maio de 2012 17:53

Respostas

  • Olá Silvio,

    Seu código da camada de acesso a dados deve ficar assim:

    public class Operacoes<T> : IOperacoes<T> where T : EntityObject { // Este é seu evento public event EventHandler ItemAdded; public bool Insert(T[] entity) { using (System.Data.Common.DbTransaction transaction = PSAInstanceDB.context.Connection.BeginTransaction()) { try {

    int count = 0; foreach (var item in entity) { PSAInstanceDB.context.AddObject(item, entity); // Este ponto é onde vc invoca seu evento if (ItemAdded != null) this.ItemAdded(this, new EventArgs());

    count++;

    if (count >= 10)

    {
    count = 0;

    PSAInstanceDB.context.SaveChanges();

    } } PSAInstanceDB.context.SaveChanges(); transaction.Commit(); return true; } catch (Exception e) { transaction.Rollback(); if (e.InnerException == null) m = new Messages(e.Message); else m = new Messages(e.InnerException.Message); return false; } } } }


    E sua camada de Controller provavelmente ficará assim:

    public void SeuMetodoInclusao()
    {
    	List<DespesaAntiga> despesasAntigas = new Despesas();
    	despesasAntigas = BusinessFactory.oDespesaAntigaBLL.SelectAll();
    	List<Despesa> novositens = new List<Despesa>();
    	foreach(DespesaAntiga despesaAntiga in despesasAntigas){
    	   Despesa despesa = new Despesa();
    	   despesa.Valor = despesaAntiga.Valor;
    	   novositens.Add(despesa);
    
    	   BusinessFactory.oDespesaBL.ItemAdded += operacoes_ItemAdded;
    	}
    
    	BusinessFactory.oDespesaBLL.Insert(novositens.ToArray());
    
    	MessageBox.Show("Processo Finalizado com Sucesso.");
    }
    
    public void operacoes_ItemAdded(object sender, EventArgs e)
    {
    	// Um item foi executado
    }
    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    sexta-feira, 25 de maio de 2012 13:45
    Moderador

Todas as Respostas

  • Olá Silvio,

    Vc chegou a testar algo assim?

    List<DespesaAntiga> despesasAntigas = new Despesas();
    despesasAntigas = BusinessFactory.oDespesaAntigaBLL.SelectAll();
    
    using (var trans = new TransactionScope())
    {
    
    	int count = 0;
    	foreach(DespesaAntiga despesaAntiga in despesasAntigas){
    	   Despesa despesa = new Despesa();
    	   despesa.Valor = despesaAntiga.Valor;
    	   BusinessFactory.oDespesaBLL.Insert(despesa);
    
    	   if (count == 10){
    		BusinessFactory.oDespesaBLL.Save();
    		count = 0;
    	   }
      	   count++;
    	}
    }
    
    
    MessageBox.Show("Processo Finalizado com Sucesso.");

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    terça-feira, 22 de maio de 2012 18:52
    Moderador
  • Vou tentrar, mas qual é o objetivo do count no código?

    terça-feira, 22 de maio de 2012 19:08
  • O objetivo é invés de executar apenas um "SaveChanges()" executar uma série deles.

    Isto limpa memória do seu DataContext, já que provavelmente ele esta acumulando muita memória na tentativa de comparar as diferenças entre os campos que foram editados com as instâncias contidas no DataContext.

    O processo de inclusão segue algum processos e rotinas internas. Provavelmente, uma quantidade muito grande de registros, sendo que as instâncias possuem muitas propriedades, pode estar causando este estouro de memória.

    Talvez fique um pouco lento, neste caso aumente o valor da comparação, de 10 para 100 ou 200.

    Vale a pena ir testando.

    Vamos ver se resolve :)

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    terça-feira, 22 de maio de 2012 19:12
    Moderador
  • Veja esta referência.

    A solução propõe uma abordagem semelhante: http://stackoverflow.com/questions/190066/what-is-the-best-way-to-use-the-savechanges-method-in-ado-net-data-services

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    terça-feira, 22 de maio de 2012 19:23
    Moderador
  • Bem, testei o seu código e deu a seguinte exceção.

    Exception: O acesso à rede do Gerenciador de Transações Distribuídas (MSDTC) foi desabilitado. Habilite o acesso à rede do DTC na configuração de segurança do MSDTC usando a ferramenta Administrativa dos Serviços de Componentes.

    InnerException: O gerenciador de transações desativou seu suporte a transações remotas/de rede. (Exceção de HRESULT: 0x8004D024)

    terça-feira, 22 de maio de 2012 19:41
  • Olá Silvio,

    Vc tem duas opções:

    1 - Comentar o TransactionScope e utilizarmos outros recurso

    ou

    2 - Acessar esse link e habilitar essa feature no server: http://www.codeproject.com/Articles/54426/MSDTC-Service-enable-issues-when-Using-NET-Transac

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    terça-feira, 22 de maio de 2012 19:49
    Moderador
  • A burocracia para mudar a feature no server é um pouco grande, então se tiver outra solução seria mais interessante para mim.

    Porém se não tiver outro jeito terei que partir para isso mesmo.

    terça-feira, 22 de maio de 2012 19:57
  • Olá Silvio,

    Veja se isto ajuda:

    List<DespesaAntiga> despesasAntigas = new Despesas();
    despesasAntigas = BusinessFactory.oDespesaAntigaBLL.SelectAll();
    
    using (System.Data.Common.DbTransaction transaction= BusinessFactory.Connection.BeginTransaction())
    {
       try
       {
    
    	int count = 0;
    	foreach(DespesaAntiga despesaAntiga in despesasAntigas){
    	   Despesa despesa = new Despesa();
    	   despesa.Valor = despesaAntiga.Valor;
    	   BusinessFactory.oDespesaBLL.Insert(despesa);
    
    	   if (count == 10){
    		BusinessFactory.oDespesaBLL.Save();
    		count = 0;
    	   }
      	   count++;
    	}
    	BusinessFactory.oDespesaBLL.Save();
    	transaction.Commit();
       }
       catch(Exception)
       {
            transaction.Rollback();
       }
    }
    
    MessageBox.Show("Processo Finalizado com Sucesso.");

    Sua classe BusinessFactory terá de expor o objeto de conexão para poder criar a transação.

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique


    terça-feira, 22 de maio de 2012 20:04
    Moderador
  • Me tira uma dúvida, minha aplicação possuí uma classe de operações generica em um projeto chamado PSA.DAL:

    public abstract class Operacoes<T> : IOperacoes<T> where T: EntityObject 
        {
            /// <summary>
            /// Propriedade Read Only que recupera o nome da Tabela no Banco de Dados.
            /// </summary>
            protected abstract string TableName { get; }
            /// <summary>
            /// Propriedade Read Only que recupera o nome da Entidade no modelo edmx.
            /// </summary>
            protected abstract string EntityName { get; }                
            /// <summary>
            /// Mensagem de excessão.
            /// </summary>
            public static Messages m;
            /*Implementação dos Métodos da IOperacoes<T>*/
            public bool Insert(T entity)
            {
                try
                {                
                    PSAInstanceDB.context.AddObject(EntityName, entity);                 
                    return true;
                }
                catch(Exception e)
                {
                    if (e.InnerException == null)
                        m = new Messages(e.Message);
                    else
                        m = new Messages(e.InnerException.Message);
                    return false;
                }
            }
    }
    Nesse projeto, também fica minha classe estática PSAInstanceDB:


    public class PSAInstanceDB
        {
            public static PSAEntities context = new PSAEntities();
            /// <summary>
            /// Define o usuário e a senha na conection string do contexto.
            /// </summary>
            /// <param name="user">usuário</param>
            /// <param name="password">senha</param>
            public static void setConnectionString(string user, string password)           
            {
                context.Connection.ConnectionString = (@"metadata=res://*/PSAModel.csdl|res://*/PSAModel.ssdl|res://*/PSAModel.msl;provider=System.Data.SqlClient;provider connection string="";data source=SERVER;initial catalog=BANCO_DADOS;user id="+user+";password="+password+@";multipleactiveresultsets=True;App=EntityFramework"";");                        
            }
        }    

    E no meu projeto BLL eu tenho a factory:

     public static class BusinessFactory
        {
            private static string user;
            public static string User
            {
                get { return BusinessFactory.user; }
                set { BusinessFactory.user = value; }
            }
            private static string password;
            public static string Password
            {
                get { return BusinessFactory.password; }
                set { BusinessFactory.password = value; }
            }
            private static DespesaBLL _DespesaBLL;
            public static DespesaBLL oDespesaBLL
            {
                get 
                { 
                    if(_DespesaBLL == null) 
                        _DespesaBLL = new DespesaBLL(new DespesaDAL());
                       
                    return _DespesaBLL;  
                          
                }
            }
    }

    Eu posso cirar um método na Facotory para gerenciar essa transaction?

    terça-feira, 22 de maio de 2012 20:31
  • Olá Silvio,

    Por questões de arquitetura e uso de dependências, acredito que o melhor seja gerenciar suas transações na camada DAL.

    Lá é o local correto.

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    quarta-feira, 23 de maio de 2012 03:27
    Moderador
  • Como eu conseguiria tratar isso levando em conta a natureza da minha classe PSAInstanceDB?
    quarta-feira, 23 de maio de 2012 13:23
  • Olá Silvio,

    Conseguimos tratar isso no nível da sua classe de Operacoes.

    Assim:

    public bool Insert(T entity)
    {
    	using (System.Data.Common.DbTransaction transaction= PSAInstanceDB.context.Connection.BeginTransaction())
    	{
    		try
    		{                
    			PSAInstanceDB.context.AddObject(EntityName, entity);     
    			transaction.Commit();
                
    			return true;
    		}
    		catch(Exception e)
    		{
    			transaction.Rollback();
    
    			if (e.InnerException == null)
    				m = new Messages(e.Message);
    			else
    				m = new Messages(e.InnerException.Message);
    			return false;
                	}
            }
    }

    []s!

    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    quarta-feira, 23 de maio de 2012 14:06
    Moderador
  • Entendi isso, mas o problema é que eu quero commitar minha transação ao fim do loop, por que se eu chamo meu médoto BusinessFactory.oDespesa.Save() em cada iteração eu não tenho OutOfMemoryException.

    Preciso de algo assim:

    //Iniciar Transaction
    try
       foreach(Classe classe in classesCollection)
       {
          //operações de insert e tudo mais
       }
       //commit
    except
    {
       //rollback
    }

    Se eu chamar desse jeito que você sugeriu, ele vai incluir o registro a cada insert no banco, ou não?

    Valeu pela força que você tá me dando.

    quarta-feira, 23 de maio de 2012 14:35
  • Olá Silvio,

    E se vc criasse uma assinatura para o método Insert da classe Operacao, que recebesse um Array de T, e que fizésse a lógica que discutimos anteriormente de inserir de N em N unidades?

    Assim todo o controle de transação fica na sua camada de acesso a dados. Parece até mais lógico isso.

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    quarta-feira, 23 de maio de 2012 14:44
    Moderador
  • Concordo com você, mas aí eu caio em um outros pequenos problemas.

    Eu tenho situações que não há collections, mas arquivos .txt, para serem importados do bom e velho sistema em Cobol de pessoal da empresa patrocinadora do fundo em que trabalho.

    E a tela para o usuário, como ficaria?

    No sistema atual eu mostro o progresso para ele.

    Eu não conheço bem a mecânica das transações no Entity Framework, mas minha ideia é criar um método dentro da DAL parecido com o da String de Conexão que eu postei ontem.

    Será que dessa forma ele aceitaria normal?

    quarta-feira, 23 de maio de 2012 17:12
  • Entendi seu problema...

    Neste caso a sua camada de controle teria conhecimento da tecnologia de acesso a dados.

    Nesta funcionalidade de importação de arquivos vc utiliza transação também?

    Da pra fazer a transação ser aberta dentro desta sua classe, mas talvez não seja o melhor cenário, ou uma boa prática... =/

    Mas se tiver de ser assim, podemos fazer...

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    quinta-feira, 24 de maio de 2012 03:40
    Moderador
  • Isso é uma situação meio complicada.

    Preciso das transações por que só quero que os dados sejam salvos no banco caso o arquivo esteja perfeito ou caso eu consiga transferir todas as informações sem problemas.

    Ao mesmo tempo não posso deixar meu sistema parado em um arquivo de 19000 linhas sem o usuário saber o progresso.

    Estou meio sem saida, por que não queria detonar minha arquitetura ao mesmo tempo tenho esse cenário.

    sexta-feira, 25 de maio de 2012 13:21
  • Olá Silvio,

    Nesse caso, o que podemos fazer para manter tudo dentro das melhores práticas é:

    1 - Fazer sua classe de acesso a dados receber um array de objetos

    2 - Abrir a transação dentro da camada de acesso a dados

    3 - Criar um evento que seja disparado toda vez que a um dos itens for iterado, assim vc consegue assinar o evento na sua controller e avisar o usuário sempre que um item for executado.

    Acho que desta forma é uma boa solução.

    Quer um código de exemplo?

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    sexta-feira, 25 de maio de 2012 13:24
    Moderador
  • Se você puder me dar um exemplo, seria ótimo.

    Por que não peguei a parte do evento.

    sexta-feira, 25 de maio de 2012 13:29
  • Olá Silvio,

    Seu código da camada de acesso a dados deve ficar assim:

    public class Operacoes<T> : IOperacoes<T> where T : EntityObject { // Este é seu evento public event EventHandler ItemAdded; public bool Insert(T[] entity) { using (System.Data.Common.DbTransaction transaction = PSAInstanceDB.context.Connection.BeginTransaction()) { try {

    int count = 0; foreach (var item in entity) { PSAInstanceDB.context.AddObject(item, entity); // Este ponto é onde vc invoca seu evento if (ItemAdded != null) this.ItemAdded(this, new EventArgs());

    count++;

    if (count >= 10)

    {
    count = 0;

    PSAInstanceDB.context.SaveChanges();

    } } PSAInstanceDB.context.SaveChanges(); transaction.Commit(); return true; } catch (Exception e) { transaction.Rollback(); if (e.InnerException == null) m = new Messages(e.Message); else m = new Messages(e.InnerException.Message); return false; } } } }


    E sua camada de Controller provavelmente ficará assim:

    public void SeuMetodoInclusao()
    {
    	List<DespesaAntiga> despesasAntigas = new Despesas();
    	despesasAntigas = BusinessFactory.oDespesaAntigaBLL.SelectAll();
    	List<Despesa> novositens = new List<Despesa>();
    	foreach(DespesaAntiga despesaAntiga in despesasAntigas){
    	   Despesa despesa = new Despesa();
    	   despesa.Valor = despesaAntiga.Valor;
    	   novositens.Add(despesa);
    
    	   BusinessFactory.oDespesaBL.ItemAdded += operacoes_ItemAdded;
    	}
    
    	BusinessFactory.oDespesaBLL.Insert(novositens.ToArray());
    
    	MessageBox.Show("Processo Finalizado com Sucesso.");
    }
    
    public void operacoes_ItemAdded(object sender, EventArgs e)
    {
    	// Um item foi executado
    }
    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    sexta-feira, 25 de maio de 2012 13:45
    Moderador
  • Parece que dessa vez vai.

    Vou testar e já te aviso.

    sexta-feira, 25 de maio de 2012 13:54
  • Olá Silvio,

    Alguma evolução nesta questão?

    []s!


    Fernando Henrique Inocêncio Borba Ferreira
    while(alive){ this.WriteCode(); }
    Blog: http://ferhenriquef.com/
    Twitter: @ferhenrique

    segunda-feira, 4 de junho de 2012 23:36
    Moderador