EF Core 5.0 -  Usando SavePoints

 Neste artigo veremos como usar o recurso SavePoints do Entity Framework Core 5.0.

O método SaveChanges é responsável por salvar todas as alterações no banco de dados, e ele é executado dentro de uma transação. Assim todas as alterações em uma única chamada para SaveChanges serão aplicadas em uma transação, se qualquer uma falhar, será feito o rollback, e nenhuma alteração será aplicada ao banco de dados.

Os SavePoints ou pontos de salvamento são pontos dentro de uma transação de banco de dados que podem ser revertidos posteriormente se ocorrer um erro.

Assim quando o método SaveChanges for chamado e uma transação já estiver em andamento, o EF Core cria um ponto de salvamento antes de salvar qualquer informação no banco de dados. Se o método SaveChanges encontrar algum erro, ele automaticamente reverte a transação para o ponto de salvamento, deixando a transação no mesmo estado como se nunca tivesse começado.

Isso permite que você corrija os problemas e tente salvar novamente, em particular quando ocorrerem problemas de concorrência otimista.

Criando e usando SavePoints

Vejamos um exemplo prático onde vamos criar um SavePoint de forma manual.

Vamos criar um projeto do tipo Console (.NET Core) no ambiente do .NET 5.0 chamado EFCore_SavePoints.

Vamos incluir neste projeto o pacote Microsoft.EntityFrameworkCore.SqlServer e também criar as pastas Context e Entities na raiz do projeto:

Na pasta Entities vamos criar as entidades : Categoria e Produto

1- Categoria

using System.Collections.Generic;
namespace EFCore_SavePoints.Entities
{
    public class Categoria
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public ICollection<Produto> Produtos { get; set; }
    }
}

2- Produto

    public class Produto
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public int CategoriaId { get; set; }
    }

Na pasta Context vamos criar o arquivo de contexto AppDbContext que herda de DbContext :

using EFCore_SavePoints.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
namespace EFCore_SavePoints.Context
{
    public class AppDbContext : DbContext
    {
        public DbSet<Categoria> Categorias { get; set; }
        public DbSet<Produto> Produtos { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(@"Data Source=.;Initial Catalog=DBDemo;Integrated Security=True")
                .EnableSensitiveDataLogging()
                .LogTo(Console.WriteLine, LogLevel.Information);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Categoria>(builder =>
            {
                builder
                    .HasMany(x => x.Produtos)
                    .WithOne()
                    .HasForeignKey(x => x.CategoriaId);
            });
        }
    }
}

 

Neste código definimos o mapeamento das entidades Categoria e Produto usando o DbSet<T> para as tabelas.

A seguir no método OnConfiguring() definimos a string de conexão do nosso ambiente e a seguir estamos usando recurso do log simples do Entity Framework Core (EF Core 5).

Os logs do EF Core podem ser acessados de qualquer tipo de aplicativo por meio do uso do método LogTo ao configurar uma instância DbContext.

O método LogTo requer um delegado Action<T> que aceita uma string. O EF Core irá chamar este delegate com uma string para cada mensagem de log gerada. Cabe então ao delegado fazer algo com a mensagem dada.

O método Console.WriteLine é freqüentemente usado para este delegate, conforme foi implementado. Isso resulta em cada mensagem de log sendo gravada no console usando o nível Information.

No método OnModelCreating estamos definindo o relacionamento um-para-muitos entre Categoria e Produtos onde uma Categoria pode ter muitos Produtos.

Agora vamos criar a classe TesteSavePoint e o método Teste() onde vamos criar um SavePoint :

using EFCore_SavePoints.Context;
using EFCore_SavePoints.Entities;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Threading.Tasks;
namespace EFCore_SavePoints
{
    public class TesteSavePoint
    {
        public async Task Teste()
        {
            using var context = new AppDbContext();
            //Cria o banco de dados e as tabelas
            await context.Database.EnsureDeletedAsync();
            await context.Database.EnsureCreatedAsync();
            Categoria categoria = null;
            Produto produto = null;
            using IDbContextTransaction transaction = context.Database.BeginTransaction();
            try
            {
                //Cria Categoria
                categoria = new Categoria() { Nome = "Categoria 1" };
                context.Categorias.Add(categoria);
                await context.SaveChangesAsync();
                await transaction.CreateSavepointAsync("Categoria_Criada");
                // Tenta incluir um produto com o valor de CategoriaId inválido
                produto = new Produto { Nome = "Produto 1", CategoriaId = 999 };
                context.Products.Add(produto);

                Console.WriteLine("######################################################");
                Console.WriteLine("######## Erro. Criando um SavePoint - Categoria_Criada \n ");
                Console.WriteLine("######################################################");

                await context.SaveChangesAsync();
                await transaction.CommitAsync();
            }
            catch (Exception)
            {
                await transaction.RollbackToSavepointAsync("Categoria_Criada");
                Console.WriteLine("################################################################");
                Console.WriteLine("######### Rollback da transação para SavePoint - Categoria_Criada \n ");
                Console.WriteLine("################################################################");
                // Remove o produto inválido existente
                context.Produtos.Local.Remove(produto);
                //Cria um novo produto válido
                produto = new Produto { Nome = "Produto 1", CategoriaId = categoria.Id };
                context.Produtos.Add(produto);
                await context.SaveChangesAsync();
                await transaction.CommitAsync();
                Console.WriteLine("#############################################");
                Console.WriteLine("######### Produto criado com sucesso\n ");
                Console.WriteLine("#############################################");
            }
        }
    }
}

Vamos entender o código acima:

Primeiro, estamos criando uma Categoria e, em seguida, tentamos incluir um novo Produto.

O salvamento do Produto lançará uma exceção porque estamos definindo o valor para CategoriaId igual a 999 que é um valor inválido.

Note que antes disso eu criamos um SavePoint chamado : 'Categoria_Criada':

await transaction.CreateSavepointAsync("Categoria_Criada");

Para criar o SavePoint usamos o método CreateSavepointAsync que cria um ponto de salvamento na transação. Isso permite que todos os comandos executados após e estabelecimento deste ponto de salvamento sejam revertidos, restaurando o estado da transação para o que era no momento do salvamento.

A seguir, dentro do bloco catch, estamos revertendo a transação para o SavePoint que criamos quando a categoria foi criada usando o método RollbackToSavepoint que reverte todos os comandos que foram executados após o estabelecimento do ponto de salvamento especificado.

A seguir estamos tentando inserir o produto novamente agora usando um valor para CategoriaId válido.

Uma coisa importante a se observar aqui é que apenas a transação está sendo revertida, tudo o que eu adicionei ao DbContext permanecerá como está.

Portanto, DEVEMOS remover o produto existente inválido e, em seguida, adicionar novamente ou atualizar/ corrigir o produto existente inválido.

Para testar vamos incluir na classe Program o código a seguir:

  public class Program
    {
        private static void Main(string[] args)
        {
            var teste = new TesteSavePoint();
            var resultado = teste.Teste();
            Console.ReadKey();
        }
    }

Executando o projeto teremos o seguinte resultado:

Pegue o projeto aqui:  EFCore_SavePoints.zip (sem as referências)

"Rogo-vos, pois, irmãos, pela compaixão de Deus, que apresenteis os vossos corpos em sacrifício vivo, santo e agradável a Deus, que é o vosso culto racional."
Romanos 12:1

Referências:


José Carlos Macoratti