Ir al contenido principal

Operaciones CRUD con bases de datos

Las operaciones CRUD (Create, Read, Update, Delete) son los cuatro pilares fundamentales para trabajar con cualquier sistema de almacenamiento de datos. Representan las acciones básicas que podemos realizar sobre nuestros datos: crear nuevos registros, leer información existente, actualizar registros y eliminar datos que ya no necesitamos. En el contexto de Entity Framework Core, estas operaciones se realizan de manera intuitiva utilizando objetos de C# y métodos que abstraen la complejidad del SQL subyacente.

La comprensión y dominio de las operaciones CRUD es esencial para cualquier desarrollador, ya que prácticamente todas las aplicaciones empresariales requieren manipular datos de alguna forma. Entity Framework Core nos proporciona herramientas poderosas para realizar estas operaciones de manera eficiente, con seguimiento automático de cambios, validaciones integradas y optimizaciones de rendimiento.

En este artículo aprenderemos a implementar cada una de las operaciones CRUD utilizando Entity Framework Core, desde las técnicas más básicas hasta patrones más avanzados, incluyendo operaciones asíncronas, validaciones y manejo de errores.

Conceptos fundamentales de CRUD

Antes de implementar las operaciones, es importante comprender qué representa cada una y cómo se mapea en el contexto de bases de datos relacionales.

Definición de operaciones CRUD

Operación Significado Acción en BD Método EF Core
Create Crear INSERT Add(), AddRange()
Read Leer SELECT Find(), Where(), ToList()
Update Actualizar UPDATE Modificar propiedades + SaveChanges()
Delete Eliminar DELETE Remove(), RemoveRange()

Estados de entidades en EF Core

Entity Framework Core rastrea el estado de cada entidad para saber qué operaciones debe ejecutar:

Estado Descripción Acción en SaveChanges
Unchanged La entidad no ha sido modificada Ninguna
Added Entidad nueva, no existe en la BD INSERT
Modified Entidad existente con cambios UPDATE
Deleted Entidad marcada para eliminación DELETE
Detached No está siendo rastreada Ninguna

Operación Create: Creando nuevos registros

La operación Create nos permite agregar nuevos registros a nuestra base de datos. EF Core proporciona varios métodos para esta tarea.

Crear un solo registro

public class LibroService
{
    private readonly BibliotecaContext _context;
    
    public LibroService(BibliotecaContext context)
    {
        _context = context;
    }
    
    // Crear un libro individual
    public async Task<Libro> CrearLibroAsync(Libro libro)
    {
        try
        {
            // Validar que el libro no sea nulo
            if (libro == null)
                throw new ArgumentNullException(nameof(libro));
            
            // Agregar el libro al contexto
            _context.Libros.Add(libro);
            
            // Guardar cambios en la base de datos
            await _context.SaveChangesAsync();
            
            Console.WriteLine($"Libro creado con ID: {libro.Id}");
            return libro;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error al crear libro: {ex.Message}");
            throw;
        }
    }
    
    // Método de conveniencia para crear libros rápidamente
    public async Task<Libro> CrearLibroRapidoAsync(string titulo, string autor, int paginas, decimal precio)
    {
        var libro = new Libro
        {
            Titulo = titulo,
            Autor = autor,
            Paginas = paginas,
            FechaPublicacion = DateTime.Now,
            Precio = precio,
            Disponible = true
        };
        
        return await CrearLibroAsync(libro);
    }
}

Crear múltiples registros

// Crear múltiples libros de una vez
public async Task<List<Libro>> CrearLibrosAsync(List<Libro> libros)
{
    try
    {
        if (libros == null || !libros.Any())
            throw new ArgumentException("La lista de libros no puede estar vacía");
        
        // Agregar todos los libros al contexto
        _context.Libros.AddRange(libros);
        
        // Guardar todos los cambios de una vez
        await _context.SaveChangesAsync();
        
        Console.WriteLine($"Se crearon {libros.Count} libros exitosamente");
        
        // Mostrar los IDs generados
        foreach (var libro in libros)
        {
            Console.WriteLine($"- {libro.Titulo}: ID {libro.Id}");
        }
        
        return libros;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error al crear libros: {ex.Message}");
        throw;
    }
}

// Ejemplo de uso para crear varios libros
public async Task EjemploCrearVariosLibros()
{
    var librosNuevos = new List<Libro>
    {
        new Libro 
        { 
            Titulo = "El nombre del viento", 
            Autor = "Patrick Rothfuss", 
            Paginas = 662, 
            FechaPublicacion = new DateTime(2007, 3, 27),
            Precio = 24.95m 
        },
        new Libro 
        { 
            Titulo = "Crónica del asesino de reyes", 
            Autor = "Patrick Rothfuss", 
            Paginas = 994, 
            FechaPublicacion = new DateTime(2011, 3, 1),
            Precio = 26.95m 
        },
        new Libro 
        { 
            Titulo = "El hobbit", 
            Autor = "J.R.R. Tolkien", 
            Paginas = 310, 
            FechaPublicacion = new DateTime(1937, 9, 21),
            Precio = 19.99m 
        }
    };
    
    await CrearLibrosAsync(librosNuevos);
}

Operación Read: Consultando datos

La operación Read es probablemente la más utilizada y versátil. EF Core ofrece múltiples formas de consultar datos.

Consultas básicas

// Obtener todos los libros
public async Task<List<Libro>> ObtenerTodosLosLibrosAsync()
{
    return await _context.Libros.ToListAsync();
}

// Buscar libro por ID
public async Task<Libro> BuscarLibroPorIdAsync(int id)
{
    // Find es optimizado para búsquedas por clave primaria
    var libro = await _context.Libros.FindAsync(id);
    
    if (libro == null)
    {
        Console.WriteLine($"No se encontró libro con ID: {id}");
    }
    
    return libro;
}

// Buscar libro por título
public async Task<Libro> BuscarLibroPorTituloAsync(string titulo)
{
    return await _context.Libros
        .FirstOrDefaultAsync(l => l.Titulo.ToLower() == titulo.ToLower());
}

// Obtener libros con filtros
public async Task<List<Libro>> BuscarLibrosDisponiblesAsync()
{
    return await _context.Libros
        .Where(l => l.Disponible)
        .OrderBy(l => l.Titulo)
        .ToListAsync();
}

Consultas avanzadas con LINQ

// Búsqueda con múltiples criterios
public async Task<List<Libro>> BuscarLibrosAvanzadoAsync(
    string autor = null, 
    int? paginasMinimas = null, 
    decimal? precioMaximo = null)
{
    var query = _context.Libros.AsQueryable();
    
    // Aplicar filtros dinámicamente
    if (!string.IsNullOrWhiteSpace(autor))
    {
        query = query.Where(l => l.Autor.Contains(autor));
    }
    
    if (paginasMinimas.HasValue)
    {
        query = query.Where(l => l.Paginas >= paginasMinimas.Value);
    }
    
    if (precioMaximo.HasValue)
    {
        query = query.Where(l => l.Precio <= precioMaximo.Value);
    }
    
    return await query
        .OrderBy(l => l.Autor)
        .ThenBy(l => l.Titulo)
        .ToListAsync();
}

// Consultas estadísticas
public async Task<dynamic> ObtenerEstadisticasLibrosAsync()
{
    var estadisticas = await _context.Libros
        .GroupBy(l => 1) // Agrupar todos los registros
        .Select(g => new
        {
            TotalLibros = g.Count(),
            LibrosDisponibles = g.Count(l => l.Disponible),
            PrecioPromedio = g.Average(l => l.Precio),
            PrecioMinimo = g.Min(l => l.Precio),
            PrecioMaximo = g.Max(l => l.Precio),
            PaginasTotal = g.Sum(l => l.Paginas)
        })
        .FirstOrDefaultAsync();
    
    return estadisticas;
}

// Paginación de resultados
public async Task<(List<Libro> libros, int totalPaginas)> ObtenerLibrosPaginadosAsync(
    int pagina = 1, 
    int tamanoPagina = 10)
{
    var totalLibros = await _context.Libros.CountAsync();
    var totalPaginas = (int)Math.Ceiling((double)totalLibros / tamanoPagina);
    
    var libros = await _context.Libros
        .OrderBy(l => l.Titulo)
        .Skip((pagina - 1) * tamanoPagina)
        .Take(tamanoPagina)
        .ToListAsync();
    
    return (libros, totalPaginas);
}

Operación Update: Actualizando registros

La operación Update nos permite modificar registros existentes. EF Core detecta automáticamente los cambios en las entidades rastreadas.

Actualización básica

// Actualizar un libro existente
public async Task<bool> ActualizarLibroAsync(int id, Libro libroActualizado)
{
    try
    {
        // Buscar el libro existente
        var libroExistente = await _context.Libros.FindAsync(id);
        
        if (libroExistente == null)
        {
            Console.WriteLine($"No se encontró libro con ID: {id}");
            return false;
        }
        
        // Actualizar propiedades
        libroExistente.Titulo = libroActualizado.Titulo;
        libroExistente.Autor = libroActualizado.Autor;
        libroExistente.Paginas = libroActualizado.Paginas;
        libroExistente.FechaPublicacion = libroActualizado.FechaPublicacion;
        libroExistente.Precio = libroActualizado.Precio;
        libroExistente.Disponible = libroActualizado.Disponible;
        
        // Guardar cambios
        await _context.SaveChangesAsync();
        
        Console.WriteLine($"Libro con ID {id} actualizado exitosamente");
        return true;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error al actualizar libro: {ex.Message}");
        return false;
    }
}

// Actualización parcial de propiedades específicas
public async Task<bool> ActualizarPrecioLibroAsync(int id, decimal nuevoPrecio)
{
    var libro = await _context.Libros.FindAsync(id);
    
    if (libro == null)
        return false;
    
    libro.Precio = nuevoPrecio;
    await _context.SaveChangesAsync();
    
    Console.WriteLine($"Precio del libro '{libro.Titulo}' actualizado a ${nuevoPrecio:F2}");
    return true;
}

// Cambiar disponibilidad de un libro
public async Task<bool> CambiarDisponibilidadLibroAsync(int id, bool disponible)
{
    var libro = await _context.Libros.FindAsync(id);
    
    if (libro == null)
        return false;
    
    libro.Disponible = disponible;
    await _context.SaveChangesAsync();
    
    string estado = disponible ? "disponible" : "no disponible";
    Console.WriteLine($"Libro '{libro.Titulo}' marcado como {estado}");
    
    return true;
}

Actualizaciones masivas

// Actualizar múltiples libros con un criterio
public async Task<int> ActualizarPreciosAutorAsync(string autor, decimal factorAumento)
{
    // Obtener libros del autor especificado
    var librosAutor = await _context.Libros
        .Where(l => l.Autor.ToLower() == autor.ToLower())
        .ToListAsync();
    
    if (!librosAutor.Any())
    {
        Console.WriteLine($"No se encontraron libros del autor: {autor}");
        return 0;
    }
    
    // Aplicar el aumento de precio
    foreach (var libro in librosAutor)
    {
        libro.Precio = libro.Precio * factorAumento;
    }
    
    await _context.SaveChangesAsync();
    
    Console.WriteLine($"Precios actualizados para {librosAutor.Count} libros de {autor}");
    return librosAutor.Count;
}

// Marcar libros como no disponibles basado en fecha
public async Task<int> MarcarLibrosAntiguosComoNoDisponiblesAsync(DateTime fechaLimite)
{
    var librosAntiguos = await _context.Libros
        .Where(l => l.FechaPublicacion < fechaLimite && l.Disponible)
        .ToListAsync();
    
    foreach (var libro in librosAntiguos)
    {
        libro.Disponible = false;
    }
    
    await _context.SaveChangesAsync();
    
    Console.WriteLine($"Se marcaron {librosAntiguos.Count} libros como no disponibles");
    return librosAntiguos.Count;
}

Operación Delete: Eliminando registros

La operación Delete nos permite remover registros de la base de datos. Debemos ser cuidadosos con esta operación ya que es irreversible.

Eliminación básica

// Eliminar un libro por ID
public async Task<bool> EliminarLibroAsync(int id)
{
    try
    {
        var libro = await _context.Libros.FindAsync(id);
        
        if (libro == null)
        {
            Console.WriteLine($"No se encontró libro con ID: {id}");
            return false;
        }
        
        // Confirmar eliminación
        Console.WriteLine($"¿Está seguro de eliminar '{libro.Titulo}' de {libro.Autor}?");
        Console.Write("Escriba 'SI' para confirmar: ");
        string confirmacion = Console.ReadLine();
        
        if (confirmacion?.ToUpper() != "SI")
        {
            Console.WriteLine("Eliminación cancelada");
            return false;
        }
        
        // Eliminar el libro
        _context.Libros.Remove(libro);
        await _context.SaveChangesAsync();
        
        Console.WriteLine($"Libro '{libro.Titulo}' eliminado exitosamente");
        return true;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error al eliminar libro: {ex.Message}");
        return false;
    }
}

// Eliminación suave (marcar como eliminado sin borrar)
public async Task<bool> EliminarSuaveLibroAsync(int id)
{
    var libro = await _context.Libros.FindAsync(id);
    
    if (libro == null)
        return false;
    
    // En lugar de eliminar, marcamos como no disponible
    libro.Disponible = false;
    await _context.SaveChangesAsync();
    
    Console.WriteLine($"Libro '{libro.Titulo}' marcado como eliminado (eliminación suave)");
    return true;
}

Eliminación masiva

// Eliminar libros por criterio
public async Task<int> EliminarLibrosPorAutorAsync(string autor)
{
    var librosAutor = await _context.Libros
        .Where(l => l.Autor.ToLower() == autor.ToLower())
        .ToListAsync();
    
    if (!librosAutor.Any())
    {
        Console.WriteLine($"No se encontraron libros del autor: {autor}");
        return 0;
    }
    
    Console.WriteLine($"Se eliminarán {librosAutor.Count} libros de {autor}:");
    foreach (var libro in librosAutor)
    {
        Console.WriteLine($"- {libro.Titulo} ({libro.FechaPublicacion.Year})");
    }
    
    Console.Write("¿Continuar con la eliminación? (SI/NO): ");
    string confirmacion = Console.ReadLine();
    
    if (confirmacion?.ToUpper() != "SI")
    {
        Console.WriteLine("Eliminación cancelada");
        return 0;
    }
    
    _context.Libros.RemoveRange(librosAutor);
    await _context.SaveChangesAsync();
    
    Console.WriteLine($"Se eliminaron {librosAutor.Count} libros exitosamente");
    return librosAutor.Count;
}

// Limpiar libros no disponibles
public async Task<int> LimpiarLibrosNoDisponiblesAsync()
{
    var librosNoDisponibles = await _context.Libros
        .Where(l => !l.Disponible)
        .ToListAsync();
    
    if (!librosNoDisponibles.Any())
    {
        Console.WriteLine("No hay libros no disponibles para eliminar");
        return 0;
    }
    
    _context.Libros.RemoveRange(librosNoDisponibles);
    await _context.SaveChangesAsync();
    
    Console.WriteLine($"Se eliminaron {librosNoDisponibles.Count} libros no disponibles");
    return librosNoDisponibles.Count;
}

Ejemplo práctico completo

Vamos a crear una aplicación de consola que demuestre todas las operaciones CRUD:

using Microsoft.EntityFrameworkCore;
using BibliotecaApp.Data;
using BibliotecaApp.Models;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("=== Operaciones CRUD con Entity Framework Core ===\n");
        
        // Configurar contexto
        var optionsBuilder = new DbContextOptionsBuilder<BibliotecaContext>();
        optionsBuilder.UseSqlite("Data Source=biblioteca_crud.db");
        
        using var context = new BibliotecaContext(optionsBuilder.Options);
        await context.Database.EnsureCreatedAsync();
        
        var servicio = new LibroService(context);
        
        try
        {
            // Demostración de operaciones CRUD
            await DemostrarOperacionesCrud(servicio);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        
        Console.WriteLine("\nPresiona cualquier tecla para salir...");
        Console.ReadKey();
    }
    
    static async Task DemostrarOperacionesCrud(LibroService servicio)
    {
        Console.WriteLine("1. === OPERACIÓN CREATE ===");
        
        // Crear libros individuales
        var libro1 = await servicio.CrearLibroRapidoAsync(
            "Don Quijote de la Mancha", 
            "Miguel de Cervantes", 
            863, 
            25.99m);
            
        var libro2 = await servicio.CrearLibroRapidoAsync(
            "Cien años de soledad", 
            "Gabriel García Márquez", 
            417, 
            22.50m);
        
        // Crear múltiples libros
        await servicio.EjemploCrearVariosLibros();
        
        Console.WriteLine("\n2. === OPERACIÓN READ ===");
        
        // Leer todos los libros
        var todosLosLibros = await servicio.ObtenerTodosLosLibrosAsync();
        Console.WriteLine($"Total de libros en la base de datos: {todosLosLibros.Count}");
        
        // Buscar por ID
        var libroBuscado = await servicio.BuscarLibroPorIdAsync(1);
        if (libroBuscado != null)
        {
            Console.WriteLine($"Libro encontrado: {libroBuscado.Titulo} por {libroBuscado.Autor}");
        }
        
        // Búsqueda avanzada
        var librosCaros = await servicio.BuscarLibrosAvanzadoAsync(precioMaximo: 25m);
        Console.WriteLine($"Libros con precio <= $25: {librosCaros.Count}");
        
        // Estadísticas
        var estadisticas = await servicio.ObtenerEstadisticasLibrosAsync();
        if (estadisticas != null)
        {
            Console.WriteLine($"Precio promedio: ${estadisticas.PrecioPromedio:F2}");
            Console.WriteLine($"Total de páginas: {estadisticas.PaginasTotal}");
        }
        
        Console.WriteLine("\n3. === OPERACIÓN UPDATE ===");
        
        // Actualizar precio
        await servicio.ActualizarPrecioLibroAsync(1, 28.99m);
        
        // Actualización masiva
        await servicio.ActualizarPreciosAutorAsync("Gabriel García Márquez", 1.1m);
        
        Console.WriteLine("\n4. === OPERACIÓN DELETE ===");
        
        // Para fines de demostración, crear un libro temporal para eliminar
        var libroTemporal = await servicio.CrearLibroRapidoAsync(
            "Libro Temporal", 
            "Autor Temporal", 
            100, 
            15.00m);
        
        Console.WriteLine($"Libro temporal creado con ID: {libroTemporal.Id}");
        
        // Eliminar el libro temporal (nota: esto requeriría confirmación del usuario)
        // await servicio.EliminarLibroAsync(libroTemporal.Id);
        
        // En su lugar, hacemos eliminación suave
        await servicio.EliminarSuaveLibroAsync(libroTemporal.Id);
        
        // Mostrar estado final
        Console.WriteLine("\n=== ESTADO FINAL ===");
        var librosFinales = await servicio.ObtenerTodosLosLibrosAsync();
        var librosDisponibles = librosFinales.Count(l => l.Disponible);
        
        Console.WriteLine($"Total de libros: {librosFinales.Count}");
        Console.WriteLine($"Libros disponibles: {librosDisponibles}");
        Console.WriteLine($"Libros no disponibles: {librosFinales.Count - librosDisponibles}");
    }
}

Buenas prácticas y consideraciones

Manejo de errores y validaciones

// Validación antes de operaciones
public async Task<bool> ValidarLibroAsync(Libro libro)
{
    var errores = new List<string>();
    
    if (string.IsNullOrWhiteSpace(libro.Titulo))
        errores.Add("El título es obligatorio");
    
    if (string.IsNullOrWhiteSpace(libro.Autor))
        errores.Add("El autor es obligatorio");
    
    if (libro.Paginas <= 0)
        errores.Add("El número de páginas debe ser mayor a cero");
    
    if (libro.Precio < 0)
        errores.Add("El precio no puede ser negativo");
    
    // Verificar duplicados
    var existe = await _context.Libros
        .AnyAsync(l => l.Titulo.ToLower() == libro.Titulo.ToLower() && 
                      l.Autor.ToLower() == libro.Autor.ToLower());
    
    if (existe)
        errores.Add("Ya existe un libro con el mismo título y autor");
    
    if (errores.Any())
    {
        Console.WriteLine("Errores de validación:");
        foreach (var error in errores)
        {
            Console.WriteLine($"- {error}");
        }
        return false;
    }
    
    return true;
}

Optimización de rendimiento

// Usar AsNoTracking para consultas de solo lectura
public async Task<List<Libro>> ObtenerLibrosSoloLecturaAsync()
{
    return await _context.Libros
        .AsNoTracking() // No rastrea cambios, mejora rendimiento
        .ToListAsync();
}

// Cargar solo las propiedades necesarias
public async Task<List<object>> ObtenerResumenLibrosAsync()
{
    return await _context.Libros
        .Select(l => new 
        {
            l.Id,
            l.Titulo,
            l.Autor,
            l.Precio
        })
        .ToListAsync();
}

Resumen

Las operaciones CRUD son la base fundamental para trabajar con datos en cualquier aplicación. En este artículo hemos explorado cómo implementar cada operación utilizando Entity Framework Core: Create para agregar nuevos registros con Add() y AddRange(), Read para consultar datos con métodos como Find(), Where() y LINQ, Update para modificar registros existentes aprovechando el seguimiento automático de cambios, y Delete para eliminar datos con Remove() y RemoveRange().

Hemos visto técnicas avanzadas como consultas dinámicas, paginación, actualizaciones masivas y eliminación suave, así como buenas prácticas para validación, manejo de errores y optimización de rendimiento. Estas operaciones forman el núcleo de la mayoría de aplicaciones empresariales y su dominio es esencial para cualquier desarrollador que trabaje con bases de datos. En el siguiente artículo profundizaremos en LINQ para consultas de bases de datos, lo que nos permitirá crear consultas más sofisticadas y expresivas.