EF Core 7 -  Os novos recursos ExecuteDelete e ExecuteUpdate - I


 Neste artigo vou apresentar os novos recursos ExecuteDelete para executar a exclusão de grandes quantidades de dados e ExecuteUpdate para atualizar uma grande quantidade de dados.

Por padrão, o EF Core rastreia alterações em entidades e, em seguida, envia atualizações para o banco de dados quando um dos métodos SaveChanges é chamado

As alterações são enviadas apenas para propriedades e relacionamentos que realmente foram alterados. Além disso, as entidades rastreadas permanecem sincronizadas com as alterações enviadas ao banco de dados. Esse mecanismo é uma maneira eficiente e conveniente de enviar inserções, atualizações e exclusões de uso geral para o banco de dados. Essas alterações também são agrupadas para reduzir o número de idas e vindas do banco de dados.

Entretanto, às vezes é útil executar comandos de atualização ou exclusão no banco de dados sem envolver o rastreador de alterações.

O EF7 permite isso com os novos métodos ExecuteUpdate e ExecuteDelete.

Esses métodos são aplicados a uma consulta LINQ e atualizarão ou excluirão entidades no banco de dados com base nos resultados dessa consulta. Muitas entidades podem ser atualizadas com um único comando e as entidades não são carregadas na memória, o que significa que isso pode resultar em atualizações e exclusões mais eficientes.

Assim, ao invés de primeiro recuperar as entidades e ter todas as entidades na memória antes de podermos executar uma ação sobre elas e, por último, confirmá-las no SQL, agora podemos fazer isso com apenas uma única operação, que resulta em um comando SQL.

No entanto existem alguns detalhes nos quais você deve ficar atento :

Dessa forma os novos métodos ExecuteUpdate e ExecuteDelete vieram para complementar o mecanismo SaveChanges existente e não para substituí-lo.

Vamos iniciar apresentando o novo método ExecuteDelete.

Para isso vamos criar uma aplicação Console usando o .NET 7.0 no VS 2022 (17.4.0) e o SQL Server.

recursos usados:

Criando o projeto Console

Abra o VS 2022 e crie um novo projeto do tipo console chamado EF7ExecuteDelete.

Após criar o projeto inclua as referências aos pacotes :

A seguir crie duas pastas no projeto :  Entities e Data

Na pasta Entities crie as classes Cliente, Endereco e Animal :

1- Cliente

public class Aluno
{
  
public int ClienteId { get; set; }
  
public string Nome { get; set; } = "";
  
public string Email { get; set; } = "";
  
public Endereco? Endereco { get; set; }
  
public List<Animal> Animais { get; set; } = new List<Animal>();
}

2- Endereco

//owned entity
public
class Endereco
{
 
public int EnderecoId { get; set; }
 
public string Local { get; set; } = null!;
}

3- Animal

//owned entity
public
class Animal
{
 
public long AnimalId { get; set; }
 
public string Nome { get; set; } = string.Empty;
 
public string Raca { get; set; } = string.Empty;
}

Na pasta Data vamos criar a classe AppDbContext que é a classe de contexto onde definimos o mapeamento ORM.

1- AppDbContext

using EF7ExecuteDelete.Entities;
using
Microsoft.EntityFrameworkCore;
namespace
EF7ExecuteDelete.Data;

public class AppDbContext : DbContext
{
  
public DbSet<Cliente> Clientes { get; set; }
  
public DbSet<Animal> Animais { get; set; }
  
public DbSet<Endereco> Enderecos { get; set; }

  
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
   { 

    optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=PetsDB;Integrated  
                                Security=True;TrustServerCertificate=True;"
)
      .LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
   }

  
protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
     
//shadow property
       modelBuilder.Entity<Endereco>()
              .Property<
int>("ClienteId");

      //shadow property
       modelBuilder.Entity<Animal>()
              .Property<
int>("ClienteId");
   }
}

Ao definir a string de conexão indicamos o nome do banco de dados a ser criado como PetsDB e também definimos na string de conexão a propriedade TrustServerCertificate=True para ignorar os mecanismos normais de segurança.

Isso é necessário pois com o .NET 7.0 houve uma alteração no pacote Microsoft.Data.SqlClient e agora a string de conexão para o SqlClient utiliza o valor Encrypt=True e com isso o servidor precisa ser configurado com um certificado válido e o cliente precisa confiar neste certificado.

Se essas condições não forem atendidas teremos uma SqlException lançada quando a conexão for acionada. Uma outra forma de mitigar o problema é definir explicitamente Encrypt=False na string de conexão.

Lembrando que ao adotar esses procedimentos em um ambiente de produção estamos colocando o servidor em um estado inseguro o que não é recomendável.

No método OnModelCreatingque é chamado pelo framework quando nosso contexto for criado pela primeira vez para construir o modelo e seu mapeamento na memória, vamos realizar o mapeamento definindo uma Shadow Property de forma a mapear Endereco e Animal para ter como chave estrangeira ClienteId.

Aplicando o Migrations usando os comandos :

Teremos a criação do banco de dados PetsDB e dsa tabelas Clientes, Enderecos e Animais com a seguinte estrutura:

 

Para poder incluir dados nas tabelas vamos criar na pasta Data a classe SeedDatabase e o método estático PopulaDB() :

public class SeedDatabase
{
    public static void PopulaDB(AppDbContext context)
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        context.Clientes.AddRange(Enumerable.Range(1, 1_000).Select(i =>
        {
            return new Cliente
            {
                Nome = $"{nameof(Cliente.Nome)}-{i}",
                Email = $"{nameof(Cliente.Email)}-{i}",
                Endereco = new Endereco
                {
                    Local = $"{nameof(Endereco.Local)}-{i}",
                },
                Animais = Enumerable.Range(1, 3).Select(i2 =>
                {
                    return new Animal
                    {
                        Raca = $"{nameof(Animal.Raca)}-{i}-{i2}",
                        Nome = $"{nameof(Animal.Nome)}-{i}-{i2}",
                    };
                }).ToList()
            };
        }));
        context.SaveChanges();
    }
}

Neste código estamos usando o método EnsureDeleted que garante que o banco de dados para o contexto não exista.

  1. Se não existir, nenhuma ação será tomada.
  2. Se existir, o banco de dados será excluído.

    Atenção: Todo o banco de dados é excluído !!!

O método EnsureCreated() que garante que o banco de dados para o contexto exista.

  1. Se existir, nenhuma ação será tomada.
  2. Se não existir, o banco de dados e todo o seu esquema são criados.
  3. Se o banco de dados existe, nenhum esforço é feito para garantir que ele seja compatível com o modelo para esse contexto.

A seguir usamos o método AddRange que anexa uma coleção de entidades Cliente, Endereco e Animal ao contexto com o estado Added (Adicionado).

Também usamos o método Enumerable.Range() que gera uma sequência de números dentro de um intervalo especificado. Para isso ele usa o método estático Range() da classe Enumerable.

A sintaxe usada é :

Enumerable.Range (int start, int count);

O retorno  é um IEnumerable<int> que contém a sequência gerada. Lembre-se de que cada número subsequente é incrementado em 1, o que também significa que os elementos estão em ordem crescente.

Para o nosso exemplo estamos incluindo 1000 registros nas tabelas Clientes e Endereços e 3000 registros na tabela Animais.

Testando o ExecuteDelete

Para testar o método ExecuteDelete vamos incluir o código abaixo na classe Program.

1- Primeiro vamos incluir dados na tabela chamando o método PopulaDB() :

using (var context = new AppDbContext())
{
   Console.WriteLine(
"\nPopulando as tabelas...");

   SeedDatabase.PopulaDB(context);

   var clientes = context.Clientes.Count();
  
var animais = context.Animais.Count();
  
var enderecos = context.Enderecos.Count();

   Console.WriteLine(
$"\nExistem {clientes} clientes");
   Console.WriteLine(
$"Existem {animais} animais");
   Console.WriteLine(
$"Existem {enderecos} endereços");
}

Executando este código terermos o resultado abaixo:

Teremos assim os dados em cada tabela e agora podemos testar a exclusão de animais usando o ExecuteDelete.

Para isso vamos executar o código abaixo na classe Program:

using (var context = new AppDbContext())
{
  
var registros = context.Animais.Count();
   Console.WriteLine(
$"\nExistem {registros} de animais na tabela\n");

   Console.WriteLine(
"\nDeletando Animais...");
  

    var
animaisExcluidos = context.Animais
                                  .Where(p => p.Nome.Contains(
"1"))
                                  .ExecuteDelete();

    Console.WriteLine($"\n{animaisExcluidos} animais foram excluidos da tabela...");
}

A consulta LINQ usada é a seguinte:

   var animaisExcluidos = context.Animais
                                 .Where(p => p.Nome.Contains(
"1"))
                                 .ExecuteDelete();

Note que temos que definir uma condição de filtro para usar o ExecuteDelete.

Observe que também podemos obter o número de linhas afetadas pela exclusão.

O resultado da execução é dada a seguir:

Como você pode ver, o EF Core simplesmente gera uma instrução SQL para excluir as entidades que correspondem à condição. As entidades também não são mais mantidas na memória. Lindo, simples e eficiente!

Vamos agora realizar uma exclusão em cascata.

Vamos excluir agora os clientes usando ExecuteDelete, e , isso vai excluir em cascata os registros de Enderecos e dos Animais relacionados.

Para isso vamos executar o código abaixo na classe Program:

using (var context = new AppDbContext())
{

   var
registros = context.Clientes.Count();
   Console.WriteLine(
$"\nExistem {registros} de clientes na tabela\n");

   Console.WriteLine("\nDeletando Clientes (em cascata)...");

   var clientesDeletados = context.Clientes
                                  .Where(p => p.ClienteId <= 500)
                                  .ExecuteDelete();

    Console.WriteLine($"\n{clientesDeletados} clientes foram excluidos da tabela...");

    var enderecos = context.Enderecos.Count();
    Console.WriteLine(
$"\nAgora existem {enderecos} enderecos na tabela...");

   var animais = context.Animais.Count();
   Console.WriteLine(
$"\nAgora existem {animais} animais na tabela...");
}

Antes de executar vamos popular novamente as tabelas do banco de dados visto que já excluimos os animais. Após isso podemos executar e obter o seguinte resultado:

Os métodos ExecuteUpdate e ExecuteDelete só podem atuar em uma única tabela. Isso tem implicações ao trabalhar com diferentes estratégias de mapeamento de herança. Geralmente, não há problemas ao usar a estratégia de mapeamento TPH, pois há apenas uma tabela para modificar.

Além disso , como já foi mencionado, pode ser necessário excluir ou atualizar entidades dependentes antes que o principal de um relacionamento possa ser excluído. Por exemplo, cada Post é dependente de seu Autor associado. Isso significa que um Autor não pode ser excluído se uma postagem ainda fizer referência a ele; isso violará a restrição de chave estrangeira no banco de dados.

Considere essas limitações ao usar este recurso.

Na próxima parte do artigo iremos apresentar o método ExecuteUpdate.

E estamos conversados...

Pegue o projeto aqui :  EF7ExecuteDelete.zip

"Eu te invoquei, ó Deus, pois me queres ouvir; inclina para mim os teus ouvidos, e escuta as minhas palavras.
Faze maravilhosas as tuas beneficências, ó tu que livras aqueles que em ti confiam dos que se levantam contra a tua destra."

Salmos 17:6,7

Referências:


José Carlos Macoratti