EF Core e PostgreSQL: o Primeiro Banco Real

EF Core e PostgreSQL: o Primeiro Banco Real

O primeiro programa C# conectado a um banco de dados real: instalação do PostgreSQL, adição do EF Core pelo NuGet, definição das classes e do contexto, criação da tabela por migração, e as operações de salvar e buscar dados usando os mesmos operadores de consulta já conhecidos.
Linguagem C#

• • 11 min de leitura

Na aula anterior, entendemos por que precisamos de bancos de dados e como o Entity Framework Core faz a ponte entre os objetos do nosso programa e as tabelas do banco. Agora colocamos isso em prática: vamos conectar, pela primeira vez, um programa C# a um banco de dados real. Usaremos o PostgreSQL, um dos bancos de dados relacionais mais respeitados e usados do mundo — poderoso, gratuito e de código aberto. Ao final desta aula, você terá um programa que salva e busca informações num banco de dados de verdade, usando o LINQ que já conhece. Como esta é a primeira vez que instalamos e configuramos um banco, iremos com cuidado, passo a passo, incluindo a instalação no WSL, como sempre. É uma aula prática e densa, mas cada passo se apoia em coisas que você já domina.

Passo 1: instalar o PostgreSQL

Antes de qualquer código, precisamos de um banco de dados rodando na máquina. No WSL/Ubuntu (o ambiente que recomendamos desde o início do curso), a instalação é feita com alguns comandos no terminal:

sudo apt-get update
sudo apt-get install -y postgresql

# Inicia o serviço do banco de dados:
sudo service postgresql start

Com o PostgreSQL instalado e o serviço iniciado, precisamos criar um usuário e um banco de dados para o nosso projeto. Entramos no console do PostgreSQL como o administrador e criamos ambos:

sudo -u postgres psql   # entra no console do PostgreSQL

Dentro desse console (você notará que o sinal de prompt muda), digite os comandos abaixo, um por vez, e depois saia com \q:

CREATE USER curso WITH PASSWORD 'senha123';
CREATE DATABASE loja_db OWNER curso;
\q

Esses comandos criam um usuário chamado curso (com uma senha) e um banco de dados chamado loja_db, do qual esse usuário é dono. (No Windows, alternativamente, você pode baixar o instalador do PostgreSQL em postgresql.org, que inclui uma interface gráfica; o restante da aula é idêntico.) Com o banco de pé, voltamos ao conforto do C#.

Passo 2: criar o projeto e adicionar o EF Core

Criamos um projeto de console e adicionamos, com o NuGet (que você aprendeu na Aula #44), as bibliotecas necessárias: o EF Core e o provedor específico do PostgreSQL — a biblioteca que ensina o EF Core a conversar com esse banco em particular:

dotnet new console -n LojaDb
cd LojaDb
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Guarde o conceito de provedor: é a biblioteca que adapta o EF Core a um banco de dados específico. O Npgsql...PostgreSQL é o provedor do PostgreSQL. Na próxima aula, veremos que trocar de banco é, em boa parte, trocar o provedor — uma demonstração do valor da abstração.

Passo 3: definir a classe e o contexto

Agora, o C#. Definimos a classe que representa o que queremos guardar — um produto, digamos. É uma classe comum, com properties, exatamente como você aprendeu; a única novidade é uma property Id, que o EF Core reconhece automaticamente como o identificador único de cada registro (a "chave" que distingue cada linha na tabela):

class Produto
{
    public int Id { get; set; }         // identificador único (o EF Core reconhece 'Id')
    public string Nome { get; set; } = "";
    public decimal Preco { get; set; }
}

Em seguida, criamos a peça central do EF Core: o contexto, uma classe que representa a conexão com o banco e expõe as tabelas como coleções. Ela herda de uma classe do EF Core (DbContext) e declara cada tabela como uma property do tipo DbSet:

using Microsoft.EntityFrameworkCore;

class LojaContext : DbContext
{
    // Cada DbSet corresponde a uma TABELA no banco. Esta é a tabela de Produtos.
    public DbSet<Produto> Produtos => Set<Produto>();

    // Configura QUAL banco usar e COMO se conectar a ele:
    protected override void OnConfiguring(DbContextOptionsBuilder opcoes)
    {
        opcoes.UseNpgsql(
            "Host=localhost;Database=loja_db;Username=curso;Password=senha123");
    }
}

Duas peças a entender. O DbSet<Produto> Produtos declara que existe uma tabela de produtos — é sobre ela que faremos as consultas LINQ. E o UseNpgsql(...), com aquela string de conexão, diz ao EF Core para usar o provedor PostgreSQL e como se conectar (o endereço do banco, o nome do banco, o usuário e a senha que criamos). Note que o contexto é um recurso que se conecta ao banco; por isso, será usado com o using que garante a liberação de recursos — como veremos no código a seguir.

Passo 4: criar a tabela a partir da classe

Nossa classe Produto descreve o que queremos guardar, mas o banco ainda está vazio — a tabela de produtos ainda não existe nele. O EF Core cria as tabelas a partir das nossas classes através de um mecanismo chamado migração (migration). Primeiro, instalamos a ferramenta de linha de comando do EF Core (uma única vez), e então geramos e aplicamos a migração:

# Instala a ferramenta do EF Core (uma vez só, para toda a máquina):
dotnet tool install --global dotnet-ef

# Gera a migração: compara as classes com o banco e descreve o que criar:
dotnet ef migrations add Inicial

# Aplica a migração: cria de fato a tabela no PostgreSQL:
dotnet ef database update

O migrations add gera um código que descreve as mudanças necessárias (criar a tabela de produtos com suas colunas); o database update executa esse código contra o banco, criando a tabela de verdade. A beleza das migrações é que, quando você mudar as classes depois (adicionar uma property, por exemplo), gera uma nova migração e o EF Core sabe atualizar o banco sem perder os dados existentes — é o controle de versão da estrutura do seu banco.

Passo 5: salvar e buscar dados

Chegou o momento que dá sentido a toda a preparação. Salvar objetos no banco é adicioná-los ao DbSet e mandar salvar. Note que operações de banco são operações de espera (conversamos com um sistema externo), então usamos a assincronia (async/await) que você conheceu na Aula #48:

using var db = new LojaContext();   // abre a conexão (liberada ao fim pelo 'using')

// INSERIR: adiciona objetos e salva no banco.
db.Produtos.Add(new Produto { Nome = "Caderno", Preco = 12.5m });
db.Produtos.Add(new Produto { Nome = "Caneta", Preco = 2m });
await db.SaveChangesAsync();   // AQUI os dados são de fato gravados no banco

// BUSCAR: usando o MESMO LINQ que você já conhece!
var baratos = await db.Produtos
    .Where(p => p.Preco < 10)         // vira uma busca no banco (não na memória!)
    .OrderBy(p => p.Nome)
    .ToListAsync();                    // executa a consulta no banco

foreach (var p in baratos)
    Console.WriteLine($"{p.Id}: {p.Nome} - R$ {p.Preco}");

Observe atentamente o que aconteceu, pois é a recompensa da fase. Para salvar, adicionamos objetos ao DbSet e chamamos SaveChangesAsync, e o EF Core os transforma em linhas na tabela. Para buscar, usamos Where e OrderBy — LINQ puro, idêntico ao que você usava em listas — mas o EF Core não trouxe todos os produtos para a memória e filtrou aqui; ele traduziu seu LINQ numa busca que o banco executou, trazendo só os resultados. É o mesmo LINQ, agora com o poder de um banco de dados por baixo. Atualizar e remover seguem a mesma lógica: você busca o objeto, altera suas properties (ou o remove do DbSet), e chama SaveChangesAsync, e o EF Core cuida de refletir isso no banco:

var produto = await db.Produtos.FirstOrDefaultAsync(p => p.Nome == "Caneta");
if (produto != null)              // tratamento de nulo (Aula #41)
{
    produto.Preco = 3m;           // o EF Core RASTREIA essa mudança
    await db.SaveChangesAsync();  // e a grava no banco automaticamente
}

O EF Core rastreia os objetos que você buscou: ao alterar uma property e salvar, ele detecta a mudança e atualiza a linha correspondente no banco, sem que você escreva nenhum comando de banco. Você trabalha com objetos; o banco reflete suas ações.

A persistência de verdade entrou em funcionamento. O PostgreSQL foi instalado e configurado, o EF Core foi adicionado ao projeto pelo NuGet, as classes do programa passaram a descrever as tabelas, e uma migração gerou a estrutura do banco a partir delas.

O detalhe que costuma surpreender é que salvar e buscar dados se pareceu muito com manipular uma coleção em memória: adicionar um objeto, consultar com os mesmos operadores já conhecidos, confirmar as alterações. Foi o ORM traduzindo tudo em comandos de banco nos bastidores. Reiniciar a aplicação e encontrar os dados intactos é a demonstração concreta do que se ganhou.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Instale o PostgreSQL (no WSL ou no Windows), crie o usuário curso e o banco loja_db, e confirme que consegue entrar no console com psql. Anote a string de conexão que você usará no C#.

Ver resposta

✓ Resposta: Após instalar e iniciar o serviço, sudo -u postgres psql abre o console; os comandos CREATE USER curso WITH PASSWORD 'senha123'; e CREATE DATABASE loja_db OWNER curso; criam o usuário e o banco. A string de conexão correspondente é Host=localhost;Database=loja_db;Username=curso;Password=senha123. Conseguir entrar no console confirma que o serviço está no ar e as credenciais funcionam.

Exercício 2

Crie o projeto LojaDb, adicione as bibliotecas do EF Core e do Npgsql, escreva a classe Produto e o LojaContext, e gere e aplique a primeira migração. Confirme, no console do PostgreSQL, que a tabela de produtos foi criada.

Ver resposta

✓ Resposta: Após criar o projeto, adicionar Microsoft.EntityFrameworkCore.Design e Npgsql.EntityFrameworkCore.PostgreSQL, escrever a classe Produto e o LojaContext, os comandos dotnet ef migrations add Inicial e dotnet ef database update criam a tabela. No console do PostgreSQL, o comando \dt lista as tabelas do banco e deve mostrar a tabela de produtos, confirmando que a estrutura foi criada a partir da classe.

Exercício 3

Escreva um programa que insira três produtos, salve-os com SaveChangesAsync, e depois busque e exiba todos os produtos com preço abaixo de um valor, ordenados por nome, usando LINQ. Confirme os resultados.

Ver resposta

✓ Resposta: Exemplo:

using var db = new LojaContext();
db.Produtos.Add(new Produto { Nome = "Lápis", Preco = 1.5m });
db.Produtos.Add(new Produto { Nome = "Régua", Preco = 4m });
db.Produtos.Add(new Produto { Nome = "Mochila", Preco = 80m });
await db.SaveChangesAsync();

var baratos = await db.Produtos.Where(p => p.Preco < 10).OrderBy(p => p.Nome).ToListAsync();
foreach (var p in baratos) Console.WriteLine($"{p.Nome}: R$ {p.Preco}");
// Exibe Lápis e Régua (Mochila fica de fora por custar 80), em ordem alfabética.

Exercício 4

Adicione uma nova property à classe Produto (por exemplo, Estoque, um inteiro), gere uma segunda migração e aplique-a. Explique, com base na aula, o que o EF Core fez ao banco sem que você perdesse os produtos já inseridos.

Ver resposta

✓ Resposta: Após adicionar public int Estoque { get; set; } à classe Produto, dotnet ef migrations add AdicionaEstoque gera uma migração descrevendo a mudança, e dotnet ef database update a aplica. O EF Core executou um comando que adicionou a nova coluna (Estoque) à tabela existente, preenchendo as linhas já presentes com o valor padrão (0, para inteiros), sem apagar nem recriar a tabela. Por isso os produtos já inseridos foram preservados: as migrações evoluem a estrutura do banco de forma incremental, adicionando o que mudou em vez de reconstruir tudo, o que mantém os dados existentes intactos.

Exercício 5

Sem código: explique o que significa dizer que, ao escrever db.Produtos.Where(p => p.Preco < 10), o filtro acontece "no banco, não na memória". Por que isso é mais eficiente do que trazer todos os produtos para o programa e filtrá-los com LINQ numa lista?

Ver resposta

✓ Resposta: Dizer que o filtro acontece "no banco, não na memória" significa que o EF Core não traz todos os produtos do banco para dentro do programa para então filtrá-los ali; em vez disso, ele traduz o Where(p => p.Preco < 10) numa instrução de busca que o banco de dados executa, e o banco devolve ao programa apenas os produtos que já satisfazem a condição. Isso é mais eficiente porque, com muitos registros, trazer todos eles para a memória do programa só para descartar a maioria seria um enorme desperdício — de tempo (transferir dados desnecessários pela conexão), de memória (carregar tudo) e de processamento. Deixar o banco fazer a filtragem aproveita que ele é especializado e otimizado para isso, e faz trafegar apenas o resultado relevante, tornando a operação muito mais rápida e econômica, especialmente em escala.

Comentários

Mais em Linguagem C#

Projeto Capstone: Reunindo Tudo
Projeto Capstone: Reunindo Tudo

O projeto que integra o curso inteiro: um gerenciador de tarefas completo, com…

Bancos de Dados em C#: Guardando Informação de Verdade
Bancos de Dados em C#: Guardando Informação de Verdade

Por que arquivos de texto não bastam para sistemas reais e o que um banco de…

Preparando o Ambiente: As Ferramentas do Programador
Preparando o Ambiente: As Ferramentas do Programador

O passo a passo para montar o ambiente de desenvolvimento C#: o SDK do .NET, o…